├── packages ├── api │ ├── src │ │ ├── util │ │ │ ├── index.ts │ │ │ └── logger │ │ │ │ ├── index.ts │ │ │ │ ├── logger.ts │ │ │ │ └── logger.test.ts │ │ ├── model │ │ │ ├── index.ts │ │ │ └── indicators-data.model.ts │ │ ├── service │ │ │ ├── email │ │ │ │ ├── index.ts │ │ │ │ └── email.service.ts │ │ │ ├── indicators-data │ │ │ │ ├── index.ts │ │ │ │ └── indicators-data.service.ts │ │ │ └── index.ts │ │ ├── repository │ │ │ ├── index.ts │ │ │ ├── indicators-data.repository.ts │ │ │ └── kinto-api.ts │ │ ├── controller │ │ │ ├── index.ts │ │ │ ├── version.controller.ts │ │ │ └── indicators-data.controller.ts │ │ ├── configuration │ │ │ ├── index.ts │ │ │ ├── __test__ │ │ │ │ └── index.spec.ts │ │ │ └── configs.ts │ │ ├── routes.ts │ │ └── server.ts │ ├── tslint.json │ ├── jest.config.js │ ├── .gitignore │ ├── Dockerfile │ ├── tsconfig.json │ └── package.json ├── app │ ├── src │ │ ├── ambient.d.ts │ │ ├── react-app-env.d.ts │ │ ├── views │ │ │ ├── FAQ │ │ │ │ ├── index.js │ │ │ │ ├── components-steps │ │ │ │ │ ├── FAQIndicateur5Steps.tsx │ │ │ │ │ ├── FAQInformationsSteps.tsx │ │ │ │ │ ├── FAQIndicateur3Steps.tsx │ │ │ │ │ ├── FAQIndicateur4Steps.tsx │ │ │ │ │ ├── FAQIndicateur2Steps.tsx │ │ │ │ │ ├── FAQEffectifsSteps.tsx │ │ │ │ │ ├── FAQIndicateur2et3Steps.tsx │ │ │ │ │ └── FAQResultatSteps.tsx │ │ │ │ ├── utils │ │ │ │ │ └── faqFuse.tsx │ │ │ │ ├── components │ │ │ │ │ ├── FAQTitle3.tsx │ │ │ │ │ ├── FAQTitle.tsx │ │ │ │ │ ├── FAQTitle2.tsx │ │ │ │ │ ├── FAQQuestionRow.tsx │ │ │ │ │ ├── FAQStep.tsx │ │ │ │ │ ├── FAQFooter.tsx │ │ │ │ │ ├── FAQCalculScale.tsx │ │ │ │ │ ├── FAQSectionRow.tsx │ │ │ │ │ └── FAQSearchBox.tsx │ │ │ │ ├── components-detail-calcul │ │ │ │ │ ├── FAQResultatDetailCalcul.tsx │ │ │ │ │ ├── FAQIndicateur5DetailCalcul.tsx │ │ │ │ │ ├── FAQIndicateur4DetailCalcul.tsx │ │ │ │ │ ├── FAQIndicateur3DetailCalcul.tsx │ │ │ │ │ └── FAQIndicateur2DetailCalcul.tsx │ │ │ │ ├── FAQHome.tsx │ │ │ │ ├── FAQSearch.tsx │ │ │ │ └── FAQQuestion.tsx │ │ │ ├── Effectif │ │ │ │ ├── index.js │ │ │ │ ├── EffectifForm.tsx │ │ │ │ └── EffectifResult.tsx │ │ │ ├── Indicateur1 │ │ │ │ ├── index.js │ │ │ │ ├── IndicateurUnResult.tsx │ │ │ │ ├── IndicateurUnCsp │ │ │ │ │ ├── IndicateurUnCsp.tsx │ │ │ │ │ └── IndicateurUnCspForm.tsx │ │ │ │ ├── IndicateurUnTypeForm.tsx │ │ │ │ └── IndicateurUnCoef │ │ │ │ │ └── components │ │ │ │ │ └── CoefGroupModalConfirmDelete.tsx │ │ │ ├── Informations │ │ │ │ ├── index.js │ │ │ │ ├── InformationsResult.tsx │ │ │ │ └── Informations.tsx │ │ │ ├── Recapitulatif │ │ │ │ └── index.js │ │ │ ├── Indicateur2 │ │ │ │ ├── index.js │ │ │ │ └── IndicateurDeuxResult.tsx │ │ │ ├── Indicateur3 │ │ │ │ ├── index.js │ │ │ │ └── IndicateurTroisResult.tsx │ │ │ ├── Indicateur5 │ │ │ │ ├── index.js │ │ │ │ ├── IndicateurCinqResult.tsx │ │ │ │ └── IndicateurCinq.tsx │ │ │ ├── Indicateur4 │ │ │ │ ├── index.js │ │ │ │ └── IndicateurQuatreResult.tsx │ │ │ ├── Indicateur2et3 │ │ │ │ ├── index.js │ │ │ │ └── IndicateurDeuxTroisResult.tsx │ │ │ └── PageNotFound.tsx │ │ ├── __fixtures__ │ │ │ ├── stateDefault.tsx │ │ │ └── stateCompleteAndValidate.tsx │ │ ├── utils │ │ │ ├── globalStyles.tsx │ │ │ ├── mapEnum.tsx │ │ │ ├── formHelpers.test.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── calculsEgaProIndicateurDeuxTrois.test.tsx.snap │ │ │ ├── hooks.tsx │ │ │ ├── merge.js │ │ │ ├── totalNombreSalaries.tsx │ │ │ ├── calculsEgaProIndex.tsx │ │ │ ├── calculsEgaProIndicateurQuatre.tsx │ │ │ ├── calculsEgaProIndicateurCinq.tsx │ │ │ ├── api.js │ │ │ └── formHelpers.tsx │ │ ├── components │ │ │ ├── ErrorMessage.tsx │ │ │ ├── TextLink.tsx │ │ │ ├── ButtonLink.tsx │ │ │ ├── ActionBar.tsx │ │ │ ├── FormAutoSave.tsx │ │ │ ├── ActionLink.tsx │ │ │ ├── ButtonSubmit.tsx │ │ │ ├── Cell.tsx │ │ │ ├── ButtonAction.tsx │ │ │ ├── LayoutFormAndResult.tsx │ │ │ ├── Bubble.tsx │ │ │ ├── Logo.tsx │ │ │ ├── ActivityIndicator.tsx │ │ │ ├── SimulatorLink.tsx │ │ │ ├── Input.tsx │ │ │ ├── ScrollContext.tsx │ │ │ ├── FormSubmit.tsx │ │ │ ├── InfoBloc.tsx │ │ │ ├── Page.tsx │ │ │ ├── Button.tsx │ │ │ ├── ResultBubble.tsx │ │ │ ├── ModalContext.tsx │ │ │ ├── Header.tsx │ │ │ ├── RadiosBoolean.tsx │ │ │ └── GridContext.tsx │ │ ├── index.tsx │ │ ├── index.css │ │ ├── App.tsx │ │ └── containers │ │ │ └── MobileLayout.tsx │ ├── public │ │ ├── marianne.png │ │ ├── manifest.json │ │ └── index.html │ ├── cypress.json │ ├── cypress │ │ ├── fixtures │ │ │ └── example.json │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── index.js │ │ │ └── commands.js │ ├── .gitignore │ ├── Dockerfile │ ├── server.js │ ├── tsconfig.json │ └── package.json └── kinto │ ├── src │ ├── config.js │ ├── index.js │ └── kinto-api.js │ ├── Dockerfile │ ├── scripts │ └── entrypoint.sh │ ├── package.json │ └── CHANGELOG.md ├── renovate.json ├── .gitignore ├── .dockerignore ├── k8s ├── kinto │ ├── service.yml │ ├── job-init-kinto.yml │ ├── .deploy-egapro-kinto.yml │ ├── deployment-prod.yml │ └── deployment.yml ├── memcached │ ├── service.yml │ ├── deployment.yml │ └── .deploy-egapro-memcached.yml ├── api │ ├── service.yml │ ├── ingress-prod.yml │ ├── ingress.yml │ ├── .deploy-egapro-api.yml │ ├── deployment-prod.yml │ └── deployment.yml ├── app │ ├── service.yml │ ├── ingress-prod.yml │ ├── ingress.yml │ ├── deployment-prod.yml │ ├── deployment.yml │ └── .deploy-egapro-app.yml ├── postgres │ ├── service.yml │ ├── .deploy-egapro-postgres.yml │ └── deployment.yml ├── scripts │ ├── init-kinto.sh │ ├── get-deploy-id.sh │ ├── send-url.sh │ └── delete-k8s-objects.py └── certificate │ └── certificate.yml ├── .editorconfig ├── lerna.json ├── .env.sample ├── package.json ├── docker-compose.yml └── .travis.yml /packages/api/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | -------------------------------------------------------------------------------- /packages/app/src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-piwik"; 2 | -------------------------------------------------------------------------------- /packages/api/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./indicators-data.model"; 2 | -------------------------------------------------------------------------------- /packages/api/src/service/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./email.service"; 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/api/src/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./indicators-data.repository"; 2 | -------------------------------------------------------------------------------- /packages/api/src/util/logger/index.ts: -------------------------------------------------------------------------------- 1 | export { default as logger } from "./logger"; 2 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/index.js: -------------------------------------------------------------------------------- 1 | import FAQ from "./FAQ"; 2 | export default FAQ; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env 4 | yarn-debug.log* 5 | yarn-error.log* 6 | -------------------------------------------------------------------------------- /packages/api/src/service/indicators-data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./indicators-data.service"; 2 | -------------------------------------------------------------------------------- /packages/api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@socialgouv/tslint-config-recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | */*/node_modules 2 | */*/dist 3 | */*/build 4 | */*/yarn-error.log 5 | */*/Dockerfile 6 | -------------------------------------------------------------------------------- /packages/api/src/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./email"; 2 | export * from "./indicators-data"; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Effectif/index.js: -------------------------------------------------------------------------------- 1 | import Effectif from "./Effectif"; 2 | export default Effectif; 3 | -------------------------------------------------------------------------------- /packages/app/public/marianne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/egapro/master/packages/app/public/marianne.png -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur1/index.js: -------------------------------------------------------------------------------- 1 | import IndicateurUn from "./IndicateurUn"; 2 | export default IndicateurUn; 3 | -------------------------------------------------------------------------------- /packages/api/src/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./indicators-data.controller"; 2 | export * from "./version.controller"; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Informations/index.js: -------------------------------------------------------------------------------- 1 | import Informations from "./Informations"; 2 | export default Informations; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Recapitulatif/index.js: -------------------------------------------------------------------------------- 1 | import Recapitulatif from "./Recapitulatif"; 2 | export default Recapitulatif; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur2/index.js: -------------------------------------------------------------------------------- 1 | import IndicateurDeux from "./IndicateurDeux"; 2 | export default IndicateurDeux; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur3/index.js: -------------------------------------------------------------------------------- 1 | import IndicateurTrois from "./IndicateurTrois"; 2 | export default IndicateurTrois; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur5/index.js: -------------------------------------------------------------------------------- 1 | import IndicateurCinq from "./IndicateurCinq"; 2 | export default IndicateurCinq; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur4/index.js: -------------------------------------------------------------------------------- 1 | import IndicateurQuatre from "./IndicateurQuatre"; 2 | export default IndicateurQuatre; 3 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur2et3/index.js: -------------------------------------------------------------------------------- 1 | import IndicateurDeuxTrois from "./IndicateurDeuxTrois"; 2 | export default IndicateurDeuxTrois; 3 | -------------------------------------------------------------------------------- /packages/api/src/model/indicators-data.model.ts: -------------------------------------------------------------------------------- 1 | export interface IndicatorsData { 2 | id?: string; 3 | // email: string; 4 | data: any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ["src/**/*.ts"], 3 | preset: "ts-jest", 4 | testEnvironment: "node" 5 | }; 6 | -------------------------------------------------------------------------------- /packages/app/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewportWidth": 1024, 3 | "viewportHeight": 660, 4 | "baseUrl": "http://localhost:3000", 5 | "video": false 6 | } 7 | -------------------------------------------------------------------------------- /packages/api/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | yarn-error.log 7 | 8 | # production 9 | dist 10 | 11 | # testing 12 | /coverage 13 | -------------------------------------------------------------------------------- /packages/app/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /packages/app/src/__fixtures__/stateDefault.tsx: -------------------------------------------------------------------------------- 1 | import AppReducer from "../AppReducer"; 2 | 3 | const stateDefault = AppReducer(undefined, { type: "initiateState", data: {} }); 4 | 5 | export default stateDefault; 6 | -------------------------------------------------------------------------------- /packages/api/src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { getConfiguration } from "./configs"; 3 | 4 | config({ path: "./../../.env" }); 5 | 6 | // 7 | 8 | export const configuration = getConfiguration(process.env); 9 | -------------------------------------------------------------------------------- /packages/kinto/src/config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | 3 | dotenv.config({ path: "./../../.env" }); 4 | 5 | module.exports.kintoURL = process.env.KINTO_URL; 6 | module.exports.adminLogin = process.env.KINTO_LOGIN; 7 | module.exports.adminPassword = process.env.KINTO_PASSWORD; 8 | -------------------------------------------------------------------------------- /k8s/kinto/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: kinto${HASH_BRANCH_NAME} 6 | labels: 7 | app: kinto${HASH_BRANCH_NAME} 8 | spec: 9 | type: NodePort 10 | ports: 11 | - port: ${PORT} 12 | selector: 13 | app: kinto${HASH_BRANCH_NAME} 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/api/src/controller/version.controller.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa"; 2 | 3 | export const versionController = { 4 | get: (ctx: Koa.Context) => { 5 | const value = require("../../package.json").version; 6 | ctx.body = { 7 | version: value ? value : "not defined" 8 | }; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": { 3 | "publish": { 4 | "skip-npm": true, 5 | "conventionalCommits": true, 6 | "message": "chore(release): version %v" 7 | } 8 | }, 9 | "npmClient": "yarn", 10 | "packages": [ 11 | "packages/*" 12 | ], 13 | "useWorkspaces": true, 14 | "version": "2.3.0" 15 | } 16 | -------------------------------------------------------------------------------- /k8s/memcached/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: cache${HASH_BRANCH_NAME} 6 | labels: 7 | app: cache${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | type: NodePort 11 | ports: 12 | - port: ${PORT} 13 | selector: 14 | app: cache${HASH_BRANCH_NAME} 15 | -------------------------------------------------------------------------------- /k8s/api/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: egapro-api${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-api${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | selector: 11 | app: egapro-api${HASH_BRANCH_NAME} 12 | ports: 13 | - port: ${PORT} 14 | name: api 15 | -------------------------------------------------------------------------------- /k8s/app/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: egapro-app${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-app${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | selector: 11 | app: egapro-app${HASH_BRANCH_NAME} 12 | ports: 13 | - port: ${PORT} 14 | name: app 15 | -------------------------------------------------------------------------------- /k8s/postgres/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: postgres${HASH_BRANCH_NAME} 6 | labels: 7 | app: postgres${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | type: NodePort 11 | ports: 12 | - port: ${PORT} 13 | selector: 14 | app: postgres${HASH_BRANCH_NAME} 15 | -------------------------------------------------------------------------------- /packages/kinto/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=10-alpine 2 | 3 | FROM node:${NODE_VERSION} 4 | 5 | ENV KINTO_SERVER=kinto-server 6 | ENV KINTO_ADMIN_LOGIN=admin 7 | ENV KINTO_ADMIN_PASSWORD=passw0rd 8 | 9 | COPY . /app 10 | WORKDIR /app 11 | RUN yarn 12 | 13 | RUN ["chmod", "+x", "./scripts/entrypoint.sh"] 14 | ENTRYPOINT ["./scripts/entrypoint.sh"] 15 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # API ENV VARIABLES 2 | KINTO_LOGIN=admin 3 | KINTO_PASSWORD=passw0rd 4 | KINTO_BUCKET=egapro 5 | MAIL_HOST=smtp.sendgrid.net 6 | MAIL_PORT=465 7 | MAIL_FROM='EgaPro ' 8 | MAIL_USERNAME=**** 9 | MAIL_PASSWORD=**** 10 | MAIL_USE_TLS=true 11 | #API_SENTRY_DSN=https://@sentry.io/ 12 | REACT_APP_API_URL=http://localhost:4000 -------------------------------------------------------------------------------- /packages/kinto/scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | export KINTO_URL=http://$KINTO_SERVER:8888/v1 4 | 5 | # Wait for kinto-server to be up 6 | while ! nc -z $KINTO_SERVER 8888; 7 | do 8 | echo sleeping; 9 | sleep 1; 10 | done; 11 | echo Connected!; 12 | 13 | # init kinto with admin account 14 | node ./src/index.js 15 | -------------------------------------------------------------------------------- /k8s/scripts/init-kinto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | INIT_KINTO_POD_STATUS=$(kubectl get job egapro-init-kinto"$1") 4 | 5 | # Check if egapro-init-kinto job exists 6 | if [ ! "$INIT_KINTO_POD_STATUS" ] 7 | then 8 | kubectl apply -f k8s/kinto/job-init-kinto-egapro.yml 9 | else 10 | kubectl delete job egapro-init-kinto"$1" 11 | kubectl apply -f k8s/kinto/job-init-kinto-egapro.yml 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /k8s/app/ingress-prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: egapro-app 6 | labels: 7 | app: egapro-app 8 | branch: egapro 9 | spec: 10 | rules: 11 | - host: ${ENVIRONMENT} 12 | http: 13 | paths: 14 | - path: / 15 | backend: 16 | serviceName: egapro-app 17 | servicePort: ${PORT} 18 | tls: 19 | - secretName: egapro-crt-secret 20 | -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /k8s/api/ingress-prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: egapro-api 6 | labels: 7 | app: egapro-api 8 | branch: egapro 9 | spec: 10 | rules: 11 | - host: egapro-api.${ENVIRONMENT} 12 | http: 13 | paths: 14 | - path: / 15 | backend: 16 | serviceName: egapro-api 17 | servicePort: ${PORT} 18 | tls: 19 | - secretName: egapro-crt-secret 20 | -------------------------------------------------------------------------------- /packages/app/src/utils/globalStyles.tsx: -------------------------------------------------------------------------------- 1 | const globalStyles = { 2 | colors: { 3 | default: "#191a49", 4 | primary: "#696CD1", 5 | men: "#447F8D", 6 | women: "#886AA7", 7 | error: "#B7585D", 8 | invalid: "#61676F" 9 | }, 10 | grid: { 11 | gutterWidth: 16, 12 | columns: 12, 13 | maxWidth: 1440, 14 | minWidth: 1024, 15 | maxTabletWidth: 1280 16 | } 17 | }; 18 | 19 | export default globalStyles; 20 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Egapro", 3 | "name": "5 indicateurs pour résorber les inégalités entre les femmes et les hommes en entreprise.", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /k8s/scripts/get-deploy-id.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -v https://${GITHUB_TOKEN}@api.github.com/repos/${CI_PROJECT_PATH}/deployments \ 4 | -H 'Content-Type:application/json' \ 5 | --data '{"ref":"'${CI_COMMIT_REF_NAME}'", "auto_merge":false, "environment": "'${CI_ENVIRONMENT_NAME}'", "required_contexts": [], "description": "Deploying '${CI_PROJECT_PATH}'@'${CI_COMMIT_SHORT_SHA}'"}' | python3 -c "import json,sys;obj=json.load(sys.stdin);print(obj.get('id'))" >> github_deploy_id 6 | -------------------------------------------------------------------------------- /packages/app/src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | function ErrorMessage(errorMessage: string | undefined) { 5 | return ( 6 |
7 |

{errorMessage || "Erreur"}

8 |
9 | ); 10 | } 11 | 12 | const styles = { 13 | errorMessage: css({ 14 | margin: "auto", 15 | alignSelf: "center" 16 | }) 17 | }; 18 | 19 | export default ErrorMessage; 20 | -------------------------------------------------------------------------------- /packages/api/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=10.15.3 2 | 3 | FROM node:${NODE_VERSION} as builder 4 | WORKDIR /app 5 | 6 | COPY packages/api . 7 | COPY yarn.lock ./yarn.lock 8 | 9 | RUN yarn 10 | RUN yarn build 11 | 12 | FROM node:${NODE_VERSION} 13 | 14 | WORKDIR /app 15 | COPY --from=builder /app/dist /app/dist 16 | COPY --from=builder /app/node_modules /app/node_modules 17 | COPY --from=builder /app/package.json /app/package.json 18 | 19 | CMD ["node", "dist/server.js"] 20 | -------------------------------------------------------------------------------- /k8s/app/ingress.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: egapro-app${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-app${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | rules: 11 | - host: ${HASH_BRANCH_INGRESS}egapro.${ENVIRONMENT}.social.gouv.fr 12 | http: 13 | paths: 14 | - path: / 15 | backend: 16 | serviceName: egapro-app${HASH_BRANCH_NAME} 17 | servicePort: ${PORT} 18 | -------------------------------------------------------------------------------- /packages/kinto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kinto", 3 | "version": "2.0.0", 4 | "main": "index.js", 5 | "author": "Incubateur des Ministères Sociaux (https://incubateur.social.gouv.fr)", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "start": "docker-compose up kinto-server init-kinto" 10 | }, 11 | "dependencies": { 12 | "btoa": "1.2.1", 13 | "dotenv": "8.2.0", 14 | "node-fetch": "2.6.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /k8s/api/ingress.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: egapro-api${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-api${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | rules: 11 | - host: ${HASH_BRANCH_INGRESS}egapro-api.${ENVIRONMENT}.social.gouv.fr 12 | http: 13 | paths: 14 | - path: / 15 | backend: 16 | serviceName: egapro-api${HASH_BRANCH_NAME} 17 | servicePort: ${PORT} 18 | -------------------------------------------------------------------------------- /k8s/certificate/certificate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: certmanager.k8s.io/v1alpha1 3 | kind: Certificate 4 | metadata: 5 | name: egapro-crt 6 | spec: 7 | secretName: egapro-crt-secret 8 | dnsNames: 9 | - ${ENVIRONMENT} 10 | - egapro-api.${ENVIRONMENT} 11 | acme: 12 | config: 13 | - http01: 14 | ingressClass: nginx 15 | domains: 16 | - ${ENVIRONMENT} 17 | - egapro-api.${ENVIRONMENT} 18 | issuerRef: 19 | name: letsencrypt-prod 20 | kind: ClusterIssuer 21 | -------------------------------------------------------------------------------- /k8s/scripts/send-url.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl \ 4 | --verbose \ 5 | https://${GITHUB_TOKEN}@api.github.com/repos/${CI_PROJECT_PATH}/deployments/${DEPLOY_ID}/statuses \ 6 | -X POST \ 7 | -H 'Content-Type: application/json' \ 8 | -H 'Accept: application/vnd.github.flash-preview+json, application/vnd.github.ant-man-preview+json' \ 9 | --data '{"environment": "'${CI_ENVIRONMENT_NAME}'", "environment_url": "'${URL}'", "log_url": "'${URL}'", "description": "Deployment finished successfully.", "state":"success"}' 10 | -------------------------------------------------------------------------------- /packages/app/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=10.15.3 2 | 3 | FROM node:${NODE_VERSION} as builder 4 | 5 | 6 | ARG REACT_APP_API_URL=http://egapro-api:4000 7 | ENV REACT_APP_API_URL="${REACT_APP_API_URL}" 8 | 9 | WORKDIR /app 10 | 11 | COPY packages/app . 12 | COPY yarn.lock ./yarn.lock 13 | 14 | RUN yarn 15 | RUN yarn build 16 | 17 | FROM node:${NODE_VERSION} 18 | WORKDIR /app 19 | COPY --from=builder /app/build ./build 20 | COPY --from=builder /app/node_modules ./node_modules 21 | COPY --from=builder /app/server.js ./server.js 22 | 23 | CMD ["node", "server.js"] 24 | -------------------------------------------------------------------------------- /packages/app/src/components/TextLink.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Link, LinkProps } from "react-router-dom"; 4 | 5 | interface Props { 6 | label: string; 7 | to: LinkProps["to"]; 8 | } 9 | 10 | function TextLink({ label, to }: Props) { 11 | return ( 12 | 13 | {label} 14 | 15 | ); 16 | } 17 | 18 | const styles = { 19 | link: css({ 20 | color: "currentColor", 21 | textDecoration: "underline" 22 | }) 23 | }; 24 | 25 | export default TextLink; 26 | -------------------------------------------------------------------------------- /packages/app/src/utils/mapEnum.tsx: -------------------------------------------------------------------------------- 1 | type EnumType = { [s: number]: string }; 2 | 3 | export default function mapEnum(enumerable: EnumType, fn: Function): any[] { 4 | // get all the members of the enum 5 | let enumMembers: any[] = Object.keys(enumerable).map( 6 | (key: any) => enumerable[key] 7 | ); 8 | 9 | // we are only interested in the numeric identifiers as these represent the values 10 | let enumValues: number[] = enumMembers.filter(v => typeof v === "number"); 11 | 12 | // now map through the enum values 13 | return enumValues.map(m => fn(m)); 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/src/utils/formHelpers.test.tsx: -------------------------------------------------------------------------------- 1 | import { mustBeDate } from "./formHelpers"; 2 | 3 | describe("mustBeDate", () => { 4 | test("parses date as ISO format", () => { 5 | expect(mustBeDate("2018-12-31")).toBe(false); 6 | expect(mustBeDate("2018-12-31a")).toBe(true); 7 | expect(mustBeDate("2018-31-12")).toBe(true); 8 | }); 9 | 10 | test("parses date as french format", () => { 11 | expect(mustBeDate("31/12/2018")).toBe(false); 12 | expect(mustBeDate("31/12/2018a")).toBe(true); 13 | expect(mustBeDate("12/31/2018")).toBe(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "react-app-polyfill/ie11"; 2 | import "react-app-polyfill/stable"; 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import "./index.css"; 6 | import App from "./App"; 7 | import * as serviceWorker from "./serviceWorker"; 8 | 9 | ReactDOM.render(, document.getElementById("root")); 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: https://bit.ly/CRA-PWA 14 | serviceWorker.unregister(); 15 | -------------------------------------------------------------------------------- /packages/app/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const path = require("path"); 3 | const app = express(); 4 | const proxy = require("http-proxy-middleware"); 5 | 6 | const apiURL = process.env.REACT_APP_API_URL; 7 | if (apiURL) { 8 | console.log("[frontend] Proxying all calls to /api to ", apiURL); 9 | app.use("/api", proxy({ target: apiURL, changeOrigin: true })); 10 | } 11 | 12 | app.use(express.static(path.join(__dirname, "build"))); 13 | 14 | app.get("/*", function (req, res) { 15 | res.sendFile(path.join(__dirname, "build", "index.html")); 16 | }); 17 | 18 | app.listen(9000); 19 | -------------------------------------------------------------------------------- /packages/app/src/components/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Link } from "react-router-dom"; 4 | import Button from "./Button"; 5 | 6 | interface Props { 7 | label: string; 8 | to: string; 9 | } 10 | 11 | function ButtonLink({ label, to }: Props) { 12 | return ( 13 | 14 | 16 | ); 17 | } 18 | 19 | const styles = { 20 | button: css({ 21 | all: "unset", 22 | 23 | padding: 0, 24 | border: "none", 25 | outline: "none", 26 | font: "inherit", 27 | color: "inherit", 28 | background: "none", 29 | 30 | cursor: "pointer", 31 | fontSize: 14, 32 | textAlign: "center", 33 | textDecoration: "underline" 34 | }) 35 | }; 36 | 37 | export default ActionLink; 38 | -------------------------------------------------------------------------------- /packages/api/src/service/indicators-data/indicators-data.service.ts: -------------------------------------------------------------------------------- 1 | import { IndicatorsData } from "../../model"; 2 | import { indicatorsDataRepository } from "../../repository"; 3 | 4 | const add = () => { 5 | const record: IndicatorsData = { 6 | data: {} 7 | }; 8 | return indicatorsDataRepository.add(record); 9 | }; 10 | 11 | const one = (id: string) => { 12 | return indicatorsDataRepository.one(id); 13 | }; 14 | 15 | const update = (record: IndicatorsData) => { 16 | return indicatorsDataRepository.update(record); 17 | }; 18 | 19 | export interface IndicatorsDataService { 20 | add: () => Promise; 21 | one: (id: string) => Promise; 22 | update: (record: IndicatorsData) => Promise; 23 | } 24 | 25 | export const indicatorsDataService: IndicatorsDataService = { 26 | add, 27 | one, 28 | update 29 | }; 30 | -------------------------------------------------------------------------------- /k8s/kinto/job-init-kinto.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Job 3 | apiVersion: batch/v1 4 | metadata: 5 | name: egapro-init-kinto${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-init-kinto 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | ttlSecondsAfterFinished: 100 11 | template: 12 | spec: 13 | containers: 14 | - image: ${EGAPRO_REGISTRY}/kinto:${IMAGE_TAG} 15 | name: egapro-init-kinto 16 | env: 17 | - name: KINTO_SERVER 18 | value: "kinto${HASH_BRANCH_NAME}" 19 | - name: KINTO_LOGIN 20 | valueFrom: 21 | secretKeyRef: 22 | name: egapro-secret 23 | key: KINTO_ADMIN_LOGIN 24 | - name: KINTO_PASSWORD 25 | valueFrom: 26 | secretKeyRef: 27 | name: egapro-secret 28 | key: KINTO_ADMIN_PASSWORD 29 | restartPolicy: Never 30 | -------------------------------------------------------------------------------- /packages/app/src/components/ButtonSubmit.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import Button from "./Button"; 4 | 5 | interface Props { 6 | label: string; 7 | outline?: boolean; 8 | error?: boolean; 9 | loading?: boolean; 10 | } 11 | 12 | function ButtonSubmit({ 13 | label, 14 | outline = false, 15 | error = false, 16 | loading = false 17 | }: Props) { 18 | return ( 19 | 22 | ); 23 | } 24 | 25 | const styles = { 26 | button: css({ 27 | all: "unset", 28 | 29 | padding: 0, 30 | border: "none", 31 | outline: "none", 32 | font: "inherit", 33 | color: "inherit", 34 | background: "none" 35 | }) 36 | }; 37 | 38 | export default ButtonSubmit; 39 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-detail-calcul/FAQResultatDetailCalcul.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconText } from "../../../components/Icons"; 6 | 7 | import FAQStep from "../components/FAQStep"; 8 | import FAQTitle3 from "../components/FAQTitle3"; 9 | 10 | function FAQResultatDetailCalcul() { 11 | return ( 12 | 13 | Calculer l’index 14 | 15 | 1}> 16 | Les indicateurs calculables doivent représenter au moins 75 points de l’Index pour que celui-ci soit calculable. 17 |
18 | Le nombre total de points ainsi obtenus est ramené sur 100 en appliquant la règle de la proportionnalité. 19 |
20 |
21 | ); 22 | } 23 | 24 | export default FAQResultatDetailCalcul; 25 | -------------------------------------------------------------------------------- /packages/app/src/utils/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | 3 | export function useDebounce(value: any, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | 19 | export function useDebounceEffect( 20 | value: any, 21 | delay: number, 22 | callback: (debouncedValue: any) => void, 23 | dep: any[] 24 | ) { 25 | const memoizedCallback = useCallback(callback, dep); 26 | 27 | const debouncedValue = useDebounce(value, delay); 28 | 29 | useEffect(() => memoizedCallback(debouncedValue), [ 30 | debouncedValue, 31 | memoizedCallback 32 | ]); 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components/FAQQuestionRow.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import globalStyles from "../../../utils/globalStyles"; 6 | 7 | interface Props { 8 | part: string; 9 | index: number; 10 | question: string; 11 | } 12 | 13 | function FAQQuestionRow({ part, index, question }: Props) { 14 | return ( 15 | 19 |

• {question}

20 | 21 | ); 22 | } 23 | 24 | const styles = { 25 | questionRow: css({ 26 | marginBottom: 12, 27 | fontSize: 14, 28 | lineHeight: "17px" 29 | }), 30 | link: css({ 31 | color: globalStyles.colors.default, 32 | textDecoration: "none" 33 | }) 34 | }; 35 | 36 | export default FAQQuestionRow; 37 | -------------------------------------------------------------------------------- /packages/app/src/components/Cell.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | 5 | interface Props { 6 | style?: any; 7 | children?: ReactNode; 8 | } 9 | 10 | function Cell({ style, children }: Props) { 11 | return
{children}
; 12 | } 13 | 14 | function Cell2({ style, children }: Props) { 15 | return
{children}
; 16 | } 17 | 18 | function CellHead({ style, children }: Props) { 19 | return
{children}
; 20 | } 21 | 22 | const styles = { 23 | cell: css({ 24 | width: 62, 25 | flexShrink: 0, 26 | marginLeft: 8 27 | }), 28 | cell2: css({ 29 | width: 62 + 8 + 62, 30 | flexShrink: 0, 31 | marginLeft: 8 32 | }), 33 | cellHead: css({ 34 | flexGrow: 1, 35 | marginRight: 2 36 | }) 37 | }; 38 | 39 | export { Cell, Cell2, CellHead }; 40 | -------------------------------------------------------------------------------- /packages/app/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /packages/api/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as cors from "@koa/cors"; 2 | import * as Sentry from "@sentry/node"; 3 | import * as Koa from "koa"; 4 | import * as bodyParser from "koa-bodyparser"; 5 | import { configuration } from "./configuration"; 6 | import { router } from "./routes"; 7 | import { logger } from "./util"; 8 | 9 | if (!!configuration.apiSentryDsn) { 10 | logger.info(`Logging to sentry DSN ${configuration.apiSentryDsn}`); 11 | logger.info(`Sentry environment is ${configuration.apiSentryEnvironment}`); 12 | Sentry.init({ 13 | debug: true, 14 | dsn: configuration.apiSentryDsn, 15 | environment: configuration.apiSentryEnvironment 16 | }); 17 | } 18 | 19 | const app = new Koa(); 20 | 21 | app.use(bodyParser()); 22 | app.use(cors()); 23 | 24 | app.use(router.routes()); 25 | app.use(router.allowedMethods()); 26 | 27 | app.on("error", (err: Error, ctx: Koa.Context) => { 28 | logger.error(`url: ${ctx.originalUrl}: `, err); 29 | }); 30 | 31 | app.listen(configuration.apiPort); 32 | -------------------------------------------------------------------------------- /packages/app/src/components/ButtonAction.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import Button from "./Button"; 4 | 5 | interface Props { 6 | label: string; 7 | onClick: () => void; 8 | outline?: boolean; 9 | error?: boolean; 10 | disabled?: boolean; 11 | loading?: boolean; 12 | } 13 | 14 | function ButtonAction({ 15 | label, 16 | onClick, 17 | outline = false, 18 | error = false, 19 | disabled = false, 20 | loading = false 21 | }: Props) { 22 | return ( 23 | 31 | ); 32 | } 33 | 34 | const styles = { 35 | button: css({ 36 | all: "unset", 37 | 38 | padding: 0, 39 | border: "none", 40 | outline: "none", 41 | font: "inherit", 42 | color: "inherit", 43 | background: "none" 44 | }) 45 | }; 46 | 47 | export default ButtonAction; 48 | -------------------------------------------------------------------------------- /packages/app/src/utils/merge.js: -------------------------------------------------------------------------------- 1 | import deepmerge from "deepmerge"; 2 | 3 | /* 4 | functions from deepmerge documentation 5 | */ 6 | 7 | export const overwriteMerge = (destinationArray, sourceArray) => sourceArray; 8 | 9 | const emptyTarget = value => (Array.isArray(value) ? [] : {}); 10 | 11 | const clone = (value, options) => deepmerge(emptyTarget(value), value, options); 12 | 13 | export const combineMerge = (target, source, options) => { 14 | const destination = target.slice(); 15 | 16 | source.forEach((item, index) => { 17 | if (typeof destination[index] === "undefined") { 18 | const cloneRequested = options.clone !== false; 19 | const shouldClone = cloneRequested && options.isMergeableObject(item); 20 | destination[index] = shouldClone ? clone(item, options) : item; 21 | } else if (options.isMergeableObject(item)) { 22 | destination[index] = deepmerge(target[index], item, options); 23 | } else if (target.indexOf(item) === -1) { 24 | destination.push(item); 25 | } 26 | }); 27 | return destination; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components/FAQStep.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | 5 | function FAQStep({ children, icon }: { children: ReactNode; icon: ReactNode }) { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
{icon}
12 |
13 | ); 14 | } 15 | 16 | const styles = { 17 | container: css({ 18 | position: "relative", 19 | marginBottom: 12, 20 | paddingLeft: 27 21 | }), 22 | background: css({ 23 | backgroundColor: "#F9F7F9", 24 | borderRadius: 5, 25 | padding: "10px 18px 10px 27px" 26 | }), 27 | text: css({ 28 | fontSize: 14, 29 | lineHeight: "17px" 30 | }), 31 | iconContainer: { 32 | left: 0, 33 | top: "50%", 34 | position: "absolute" as "absolute", // Because typescript… 35 | marginTop: -22, 36 | width: 44, 37 | height: 44 38 | } 39 | }; 40 | 41 | export default FAQStep; 42 | -------------------------------------------------------------------------------- /packages/api/src/repository/indicators-data.repository.ts: -------------------------------------------------------------------------------- 1 | import { IndicatorsData } from "../model"; 2 | import { collection, KintoCollection } from "./kinto-api"; 3 | 4 | const kintoCollection: KintoCollection = collection< 5 | IndicatorsData 6 | >("indicators_datas"); 7 | 8 | const add: (record: IndicatorsData) => Promise = async ( 9 | record: IndicatorsData 10 | ) => { 11 | const kintoResult = await kintoCollection.add(record); 12 | return kintoResult.data; 13 | }; 14 | 15 | const one: (id: string) => Promise = async (id: string) => { 16 | const kintoResult = await kintoCollection.one(id); 17 | return kintoResult.data; 18 | }; 19 | 20 | const update: (record: IndicatorsData) => Promise = async ( 21 | record: IndicatorsData 22 | ) => { 23 | if (!record.id) { 24 | throw new Error("Try to update a record without id"); 25 | } 26 | const kintoResult = await kintoCollection.update(record.id, record); 27 | return kintoResult.data; 28 | }; 29 | 30 | export const indicatorsDataRepository = { 31 | add, 32 | one, 33 | update 34 | }; 35 | -------------------------------------------------------------------------------- /k8s/app/deployment-prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Deployment 3 | apiVersion: apps/v1 4 | metadata: 5 | name: egapro-app${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-app${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: egapro-app${HASH_BRANCH_NAME} 14 | template: 15 | metadata: 16 | labels: 17 | app: egapro-app${HASH_BRANCH_NAME} 18 | spec: 19 | containers: 20 | - image: ${EGAPRO_REGISTRY}/app:${IMAGE_TAG} 21 | name: egapro-app 22 | ports: 23 | - containerPort: ${PORT} 24 | livenessProbe: 25 | httpGet: 26 | path: / 27 | port: ${PORT} 28 | initialDelaySeconds: 3 29 | periodSeconds: 5 30 | readinessProbe: 31 | httpGet: 32 | path: / 33 | port: ${PORT} 34 | initialDelaySeconds: 3 35 | periodSeconds: 5 36 | env: 37 | - name: REACT_APP_API_URL 38 | value: "https://egapro-api.${ENVIRONMENT}" 39 | -------------------------------------------------------------------------------- /k8s/postgres/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: postgres${HASH_BRANCH_NAME} 6 | labels: 7 | app: postgres${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: postgres${HASH_BRANCH_NAME} 15 | branch: egapro${HASH_BRANCH_NAME} 16 | spec: 17 | containers: 18 | - name: postgres 19 | image: postgres:11-alpine 20 | imagePullPolicy: "IfNotPresent" 21 | ports: 22 | - containerPort: ${PORT} 23 | env: 24 | - name: POSTGRES_USER 25 | valueFrom: 26 | secretKeyRef: 27 | name: egapro-secret 28 | key: DB_USER 29 | - name: POSTGRES_PASSWORD 30 | valueFrom: 31 | secretKeyRef: 32 | name: egapro-secret 33 | key: DB_PASSWORD 34 | volumeMounts: 35 | - mountPath: /var/lib/postgresql/data 36 | name: postgredb 37 | volumes: 38 | - name: postgredb 39 | emptyDir: {} 40 | -------------------------------------------------------------------------------- /k8s/app/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Deployment 3 | apiVersion: apps/v1 4 | metadata: 5 | name: egapro-app${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-app${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: egapro-app${HASH_BRANCH_NAME} 14 | template: 15 | metadata: 16 | labels: 17 | app: egapro-app${HASH_BRANCH_NAME} 18 | spec: 19 | containers: 20 | - image: ${EGAPRO_REGISTRY}/app:${IMAGE_TAG} 21 | name: egapro-app 22 | ports: 23 | - containerPort: ${PORT} 24 | livenessProbe: 25 | httpGet: 26 | path: / 27 | port: ${PORT} 28 | initialDelaySeconds: 3 29 | periodSeconds: 5 30 | readinessProbe: 31 | httpGet: 32 | path: / 33 | port: ${PORT} 34 | initialDelaySeconds: 3 35 | periodSeconds: 5 36 | env: 37 | - name: REACT_APP_API_URL 38 | value: "http://${HASH_BRANCH_INGRESS}egapro-api.${ENVIRONMENT}.social.gouv.fr" 39 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQInformationsSteps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconCalendar } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQInformationsSteps() { 9 | return ( 10 | 11 | }> 12 | Les indicateurs sont calculés à partir des données de la période de 13 | référence annuelle choisie par l’entreprise.{" "} 14 | 15 | Cette période doit être de 12 mois consécutifs et précéder l’année de 16 | publication. 17 | {" "} 18 | Elle doit donc nécessairement s’achever au plus tard le 31 décembre 2019 19 | pour un Index publié en 2020. Uniquement pour l’indicateur « écart de 20 | taux d’augmentations », et uniquement pour une entreprise de 50 à 250 21 | salariés, l’employeur peut choisir une période de référence de 3 ans 22 | maximum. Ce caractère pluriannuel peut être révisé tous les 3 ans. 23 | 24 | 25 | ); 26 | } 27 | 28 | export default FAQInformationsSteps; 29 | -------------------------------------------------------------------------------- /k8s/memcached/.deploy-egapro-memcached.yml: -------------------------------------------------------------------------------- 1 | --- 2 | .deploy-egapro-memcached-k8s-dev: 3 | image: 4 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 5 | entrypoint: [""] 6 | before_script: 7 | - /apps/create-kubeconfig.sh 8 | - HASH_BRANCH_NAME=$(printf "$CI_COMMIT_REF_NAME" | sha1sum | cut -c1-5) 9 | - export HASH_BRANCH_NAME=-$HASH_BRANCH_NAME 10 | - envsubst < k8s/memcached/deployment.yml > k8s/memcached/deployment-egapro.yml 11 | - envsubst < k8s/memcached/service.yml > k8s/memcached/service-egapro.yml 12 | script: 13 | - kubectl apply -f k8s/memcached/deployment-egapro.yml 14 | - kubectl apply -f k8s/memcached/service-egapro.yml 15 | allow_failure: false 16 | 17 | .deploy-egapro-memcached-k8s-prod: 18 | image: 19 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 20 | entrypoint: [""] 21 | before_script: 22 | - /apps/create-kubeconfig.sh 23 | - envsubst < k8s/memcached/deployment.yml > k8s/memcached/deployment-egapro.yml 24 | - envsubst < k8s/memcached/service.yml > k8s/memcached/service-egapro.yml 25 | script: 26 | - kubectl apply -f k8s/memcached/deployment-egapro.yml 27 | - kubectl apply -f k8s/memcached/service-egapro.yml 28 | allow_failure: false 29 | -------------------------------------------------------------------------------- /packages/app/src/components/LayoutFormAndResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | import { useColumnsWidth, useLayoutType } from "../components/GridContext"; 8 | 9 | interface Props { 10 | childrenForm: ReactNode; 11 | childrenResult: ReactNode; 12 | } 13 | 14 | function LayoutFormAndResult({ childrenForm, childrenResult }: Props) { 15 | const layoutType = useLayoutType(); 16 | const width = useColumnsWidth(layoutType === "desktop" ? 4 : 5); 17 | return ( 18 |
19 |
{childrenForm}
20 |
{childrenResult}
21 |
22 | ); 23 | } 24 | 25 | const styles = { 26 | body: css({ 27 | display: "flex", 28 | flexDirection: "row", 29 | alignItems: "flex-start" 30 | }), 31 | result: css({ 32 | flexGrow: 1, 33 | flexShrink: 1, 34 | flexBasis: "0%", 35 | marginLeft: globalStyles.grid.gutterWidth, 36 | position: "sticky", 37 | top: 0, 38 | display: "flex", 39 | flexDirection: "column" 40 | }) 41 | }; 42 | 43 | export default LayoutFormAndResult; 44 | -------------------------------------------------------------------------------- /packages/app/src/components/Bubble.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | export interface Props { 8 | children: ReactNode; 9 | style?: any; 10 | } 11 | 12 | function Bubble({ children, style }: Props) { 13 | return ( 14 |
15 |
{children}
16 |
17 | ); 18 | } 19 | 20 | const styles = { 21 | container: css({ 22 | width: "100%", 23 | position: "relative", 24 | height: 0, 25 | paddingTop: "100%" 26 | }), 27 | bloc: css({ 28 | position: "absolute", 29 | top: 0, 30 | bottom: 0, 31 | left: 0, 32 | right: 0, 33 | 34 | padding: "26% 8%", 35 | display: "flex", 36 | flexDirection: "column", 37 | justifyContent: "space-between", 38 | 39 | color: "white", 40 | backgroundColor: globalStyles.colors.default, 41 | borderRadius: "100%", 42 | "@media print": { 43 | backgroundColor: "white", 44 | color: globalStyles.colors.default, 45 | border: `solid ${globalStyles.colors.default} 1px` 46 | } 47 | }) 48 | }; 49 | 50 | export default Bubble; 51 | -------------------------------------------------------------------------------- /packages/app/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | function Logo({ layout = "default" }: { layout?: "default" | "mobile" }) { 5 | return ( 6 |
9 | Ministère du Travail 14 | Ministère du Travail 15 |
16 | ); 17 | } 18 | 19 | const styles = { 20 | container: css({ 21 | flexShrink: 0, 22 | display: "flex", 23 | flexDirection: "column", 24 | width: 76 25 | }), 26 | containerMobile: css({ 27 | flexDirection: "row", 28 | alignItems: "flex-end", 29 | width: "auto" 30 | }), 31 | image: css({ 32 | display: "block", 33 | width: 76, 34 | height: 46, 35 | border: "solid black 1px" 36 | }), 37 | imageMobile: css({ 38 | width: 60, 39 | height: 36, 40 | marginRight: 3 41 | }), 42 | text: css({ 43 | marginTop: 3, 44 | fontSize: 9, 45 | textAlign: "center" 46 | }) 47 | }; 48 | 49 | export default Logo; 50 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components/FAQFooter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import globalStyles from "../../../utils/globalStyles"; 4 | 5 | function FAQFooter() { 6 | return ( 7 |
8 |
9 | 10 | Vous n’avez pas trouvé l’aide que vous cherchiez ? 11 | 12 |
13 | 14 | 18 | Contactez votre référent égapro 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | const styles = { 27 | container: css({ 28 | marginTop: "auto", 29 | height: 80, 30 | flexShrink: 0, 31 | display: "flex", 32 | flexDirection: "row", 33 | alignItems: "center", 34 | borderTop: "1px solid #EFECEF" 35 | }), 36 | text: css({ 37 | fontSize: 12, 38 | lineHeight: "15px" 39 | }), 40 | infoLink: { 41 | color: globalStyles.colors.default, 42 | textDecoration: "underline", 43 | marginLeft: 8 44 | } 45 | }; 46 | 47 | export default FAQFooter; 48 | -------------------------------------------------------------------------------- /packages/api/src/configuration/__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import { getConfiguration } from "../configs"; 4 | 5 | const validEnv = { 6 | API_PORT: "123456", 7 | API_SENTRY_DSN: "API_SENTRY_DSN", 8 | API_SENTRY_ENVIRONMENT: "API_SENTRY_ENVIRONMENT", 9 | KINTO_BUCKET: "KINTO_BUCKET", 10 | KINTO_LOGIN: "KINTO_LOGIN", 11 | KINTO_PASSWORD: "KINTO_PASSWORD", 12 | KINTO_SERVER: "KINTO_SERVER", 13 | MAIL_FROM: "MAIL_FROM", 14 | MAIL_HOST: "MAIL_HOST", 15 | MAIL_PASSWORD: "MAIL_PASSWORD", 16 | MAIL_PORT: "465", 17 | MAIL_USERNAME: "MAIL_USERNAME", 18 | MAIL_USE_TLS: "true" 19 | }; 20 | 21 | it("should return the env configuration", () => { 22 | expect(getConfiguration(validEnv)).toMatchInlineSnapshot(` 23 | Object { 24 | "apiPort": 123456, 25 | "apiSentryDsn": "API_SENTRY_DSN", 26 | "apiSentryEnvironment": "API_SENTRY_ENVIRONMENT", 27 | "kintoBucket": "KINTO_BUCKET", 28 | "kintoLogin": "KINTO_LOGIN", 29 | "kintoPassword": "KINTO_PASSWORD", 30 | "kintoURL": "http://KINTO_SERVER:8888/v1", 31 | "mailFrom": "MAIL_FROM", 32 | "mailHost": "MAIL_HOST", 33 | "mailPassword": "MAIL_PASSWORD", 34 | "mailPort": 465, 35 | "mailUseTLS": true, 36 | "mailUsername": "MAIL_USERNAME", 37 | } 38 | `); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/app/src/components/ActivityIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx, keyframes } from "@emotion/core"; 3 | 4 | interface Props { 5 | size?: number; 6 | color?: string; 7 | } 8 | 9 | function ActivityIndicator({ size = 25, color = "#FFF" }: Props) { 10 | return ( 11 |
12 |
13 |
20 |
21 | ); 22 | } 23 | 24 | const bounce = keyframes({ 25 | "0%": { 26 | transform: "scale(0)" 27 | }, 28 | "50%": { 29 | transform: "scale(1)" 30 | }, 31 | "100%": { 32 | transform: "scale(0)" 33 | } 34 | }); 35 | 36 | const stylesActivity = { 37 | container: css({ 38 | position: "relative" 39 | }), 40 | round: css({ 41 | position: "absolute", 42 | top: 0, 43 | left: 0, 44 | right: 0, 45 | bottom: 0, 46 | 47 | borderRadius: "50%", 48 | opacity: 0.6, 49 | animation: `${bounce} 2.0s infinite ease-in-out` 50 | }), 51 | round2: css({ 52 | animationDelay: "-1.0s" 53 | }) 54 | }; 55 | 56 | export default ActivityIndicator; 57 | -------------------------------------------------------------------------------- /packages/app/src/components/SimulatorLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { RouteComponentProps, withRouter } from "react-router-dom"; 3 | 4 | import ButtonLink from "./ButtonLink"; 5 | import TextLink from "./TextLink"; 6 | 7 | interface MatchParams { 8 | code: string; 9 | } 10 | 11 | interface SimulatorLinkProps extends RouteComponentProps { 12 | children: (to: string) => ReactElement; 13 | } 14 | 15 | function SimulatorLink({ 16 | children, 17 | match: { 18 | params: { code } 19 | } 20 | }: SimulatorLinkProps) { 21 | return children(`/simulateur/${code}`); 22 | } 23 | 24 | const SimulatorLinkWithRouter = withRouter(SimulatorLink); 25 | 26 | export default SimulatorLinkWithRouter; 27 | 28 | interface LinkProps { 29 | label: string; 30 | to: string; 31 | } 32 | 33 | export function ButtonSimulatorLink({ to, label }: LinkProps) { 34 | return ( 35 | 36 | {toSimulator => } 37 | 38 | ); 39 | } 40 | 41 | export function TextSimulatorLink({ to, label }: LinkProps) { 42 | return ( 43 | 44 | {toSimulator => } 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/FAQHome.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import FAQSearch from "./FAQSearch"; 5 | import FAQSectionRow from "./components/FAQSectionRow"; 6 | 7 | import { faqSections, faqData } from "../../data/faq"; 8 | 9 | const faqSectionsEntries = Object.entries(faqSections); 10 | 11 | function FAQHome() { 12 | return ( 13 |
14 | 15 |
16 | {faqSectionsEntries.map(([faqKey, faqSection]) => { 17 | const questionsLength = faqSection.parts.reduce( 18 | (acc, part) => acc + faqData[part].qr.length, 19 | 0 20 | ); 21 | return ( 22 | 1 ? "s" : "" 28 | }`} 29 | /> 30 | ); 31 | })} 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | const styles = { 39 | container: css({}), 40 | content: css({ 41 | marginTop: 14, 42 | marginBottom: 14 43 | }) 44 | }; 45 | 46 | export default FAQHome; 47 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components/FAQCalculScale.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | interface Props { 5 | listTitle: string; 6 | list: Array; 7 | scaleTitle: string; 8 | scale: Array; 9 | } 10 | 11 | function FAQCalculScale({ listTitle, list, scaleTitle, scale }: Props) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {list.map((listEl, index) => ( 22 | 23 | 24 | 25 | 26 | ))} 27 | 28 |
{listTitle}{scaleTitle}
• {listEl}{scale[index]}
29 | ); 30 | } 31 | 32 | const styles = { 33 | container: css({ 34 | marginBottom: 12, 35 | padding: "13px 25px", 36 | backgroundColor: "#F9F7F9", 37 | borderRadius: 5, 38 | width: "100%" 39 | }), 40 | text: css({ 41 | fontSize: 14, 42 | lineHeight: "17px" 43 | }), 44 | title: css({ 45 | paddingBottom: 8, 46 | fontWeight: "bold", 47 | textAlign: "left" 48 | }) 49 | }; 50 | 51 | export default FAQCalculScale; 52 | -------------------------------------------------------------------------------- /packages/app/src/utils/totalNombreSalaries.tsx: -------------------------------------------------------------------------------- 1 | export default function( 2 | nombreSalaries: Array<{ 3 | tranchesAges: Array<{ 4 | nombreSalariesHommes: number | undefined; 5 | nombreSalariesFemmes: number | undefined; 6 | }>; 7 | }> 8 | ) { 9 | return nombreSalaries.reduce( 10 | (acc, { tranchesAges }) => { 11 | const { 12 | totalGroupNombreSalariesHomme, 13 | totalGroupNombreSalariesFemme 14 | } = tranchesAges.reduce( 15 | (accGroup, { nombreSalariesHommes, nombreSalariesFemmes }) => { 16 | return { 17 | totalGroupNombreSalariesHomme: 18 | accGroup.totalGroupNombreSalariesHomme + 19 | (nombreSalariesHommes || 0), 20 | totalGroupNombreSalariesFemme: 21 | accGroup.totalGroupNombreSalariesFemme + 22 | (nombreSalariesFemmes || 0) 23 | }; 24 | }, 25 | { totalGroupNombreSalariesHomme: 0, totalGroupNombreSalariesFemme: 0 } 26 | ); 27 | 28 | return { 29 | totalNombreSalariesHomme: 30 | acc.totalNombreSalariesHomme + totalGroupNombreSalariesHomme, 31 | totalNombreSalariesFemme: 32 | acc.totalNombreSalariesFemme + totalGroupNombreSalariesFemme 33 | }; 34 | }, 35 | { totalNombreSalariesHomme: 0, totalNombreSalariesFemme: 0 } 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-detail-calcul/FAQIndicateur5DetailCalcul.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp } from "../../../components/Icons"; 6 | 7 | import FAQStep from "../components/FAQStep"; 8 | import FAQCalculScale from "../components/FAQCalculScale"; 9 | import FAQTitle3 from "../components/FAQTitle3"; 10 | 11 | function FAQIndicateur5DetailCalcul() { 12 | return ( 13 | 14 | Calculer l’indicateur 15 | 16 | }> 17 | Comparer le nombre de femmes et le nombre d’hommes comptant parmi les 10 18 | plus hautes rémunérations de l’entreprise. 19 | 20 | 21 |
22 | Appliquer le barème pour obtenir votre note 23 | 24 | 30 |
31 |
32 | ); 33 | } 34 | 35 | const styles = { 36 | content: css({ 37 | marginTop: 30 38 | }) 39 | }; 40 | 41 | export default FAQIndicateur5DetailCalcul; 42 | -------------------------------------------------------------------------------- /packages/app/src/index.css: -------------------------------------------------------------------------------- 1 | @import "~normalize.css"; 2 | 3 | @import url("https://fonts.googleapis.com/css?family=Cabin:400,700&display=swap"); 4 | 5 | @import url("https://fonts.googleapis.com/css?family=Gabriela&display=swap"); 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | background-color: #fff; 13 | color: #191a49; 14 | } 15 | 16 | body { 17 | font-family: "Cabin", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 18 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 19 | sans-serif; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | 23 | display: flex; 24 | height: 100vh; 25 | overflow: hidden; 26 | } 27 | 28 | #root { 29 | display: flex; 30 | flex: 1 1 0%; 31 | } 32 | 33 | @media print { 34 | body { 35 | display: block; 36 | height: auto; 37 | overflow: visible; 38 | } 39 | #root { 40 | display: block; 41 | } 42 | } 43 | 44 | p { 45 | margin: 0; 46 | } 47 | 48 | input[type="number"] { 49 | -moz-appearance: textfield; 50 | } 51 | 52 | input::-webkit-inner-spin-button, 53 | input::-webkit-outer-spin-button { 54 | -webkit-appearance: none; 55 | margin: 0; 56 | } 57 | 58 | svg { 59 | display: block; 60 | } 61 | 62 | *:focus { 63 | outline: auto #696cd1 2px; 64 | } 65 | 66 | *:active { 67 | outline: none; 68 | } 69 | -------------------------------------------------------------------------------- /k8s/kinto/.deploy-egapro-kinto.yml: -------------------------------------------------------------------------------- 1 | --- 2 | .deploy-egapro-kinto-k8s-dev: 3 | image: 4 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 5 | entrypoint: [""] 6 | before_script: 7 | - /apps/create-kubeconfig.sh 8 | - HASH_BRANCH_NAME=$(printf "$CI_COMMIT_REF_NAME" | sha1sum | cut -c1-5) 9 | - export HASH_BRANCH_NAME=-$HASH_BRANCH_NAME 10 | - envsubst < k8s/kinto/deployment.yml > k8s/kinto/deployment-egapro.yml 11 | - envsubst < k8s/kinto/service.yml > k8s/kinto/service-egapro.yml 12 | - envsubst < k8s/kinto/job-init-kinto.yml > k8s/kinto/job-init-kinto-egapro.yml 13 | script: 14 | - kubectl apply -f k8s/kinto/deployment-egapro.yml 15 | - kubectl apply -f k8s/kinto/service-egapro.yml 16 | - k8s/scripts/init-kinto.sh $HASH_BRANCH_NAME 17 | allow_failure: false 18 | 19 | .deploy-egapro-kinto-k8s-prod: 20 | image: 21 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 22 | entrypoint: [""] 23 | before_script: 24 | - /apps/create-kubeconfig.sh 25 | - envsubst < k8s/kinto/deployment-prod.yml > k8s/kinto/deployment-prod-egapro.yml 26 | - envsubst < k8s/kinto/service.yml > k8s/kinto/service-egapro.yml 27 | - envsubst < k8s/kinto/job-init-kinto.yml > k8s/kinto/job-init-kinto-egapro.yml 28 | script: 29 | - kubectl apply -f k8s/kinto/deployment-prod-egapro.yml 30 | - kubectl apply -f k8s/kinto/service-egapro.yml 31 | - k8s/scripts/init-kinto.sh $HASH_BRANCH_NAME 32 | allow_failure: false 33 | -------------------------------------------------------------------------------- /k8s/api/.deploy-egapro-api.yml: -------------------------------------------------------------------------------- 1 | --- 2 | .deploy-egapro-api-k8s-dev: 3 | image: 4 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 5 | entrypoint: [""] 6 | before_script: 7 | - /apps/create-kubeconfig.sh 8 | - HASH_BRANCH_NAME=$(printf "$CI_COMMIT_REF_NAME" | sha1sum | cut -c1-5) 9 | - export HASH_BRANCH_INGRESS=$HASH_BRANCH_NAME- 10 | - export HASH_BRANCH_NAME=-$HASH_BRANCH_NAME 11 | - envsubst < k8s/api/deployment.yml > k8s/api/deployment-egapro.yml 12 | - envsubst < k8s/api/service.yml > k8s/api/service-egapro.yml 13 | - envsubst < k8s/api/ingress.yml > k8s/api/ingress-egapro.yml 14 | script: 15 | - kubectl apply -f k8s/api/deployment-egapro.yml 16 | - kubectl apply -f k8s/api/service-egapro.yml 17 | - kubectl apply -f k8s/api/ingress-egapro.yml 18 | allow_failure: false 19 | 20 | .deploy-egapro-api-k8s-prod: 21 | image: 22 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 23 | entrypoint: [""] 24 | before_script: 25 | - /apps/create-kubeconfig.sh 26 | - envsubst < k8s/api/deployment-prod.yml > k8s/api/deployment-egapro.yml 27 | - envsubst < k8s/api/service.yml > k8s/api/service-egapro.yml 28 | - envsubst < k8s/api/ingress-prod.yml > k8s/api/ingress-prod-egapro.yml 29 | script: 30 | - kubectl apply -f k8s/api/deployment-egapro.yml 31 | - kubectl apply -f k8s/api/service-egapro.yml 32 | - kubectl apply -f k8s/api/ingress-prod-egapro.yml 33 | allow_failure: false 34 | -------------------------------------------------------------------------------- /packages/app/src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { FieldRenderProps, FieldMetaState } from "react-final-form"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | export const hasFieldError = (meta: FieldMetaState) => 8 | (meta.error && meta.submitFailed) || 9 | (meta.error && 10 | meta.touched && 11 | Object.values({ ...meta.error, required: false }).includes(true)); 12 | 13 | interface Props { 14 | field: FieldRenderProps; 15 | placeholder?: string; 16 | style?: any; 17 | readOnly?: boolean; 18 | } 19 | 20 | function Input({ 21 | field: { input, meta }, 22 | placeholder, 23 | style, 24 | readOnly = false 25 | }: Props) { 26 | const error = hasFieldError(meta); 27 | 28 | return ( 29 | 37 | ); 38 | } 39 | 40 | const styles = { 41 | input: css({ 42 | appearance: "none", 43 | border: `solid ${globalStyles.colors.default} 1px`, 44 | width: "100%", 45 | fontSize: 14, 46 | lineHeight: "17px", 47 | paddingLeft: 22, 48 | paddingRight: 22 49 | }), 50 | inputError: css({ 51 | color: globalStyles.colors.error, 52 | borderColor: globalStyles.colors.error 53 | }) 54 | }; 55 | 56 | export default Input; 57 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQIndicateur3Steps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp, IconGrow } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQIndicateur3Steps() { 9 | return ( 10 | 11 | 12 | Indicateur concernant les entreprises de plus de 250 salariés 13 | 14 |
15 |
16 | 17 | }> 18 | La notion de promotion correspond au{" "} 19 | 20 | passage à un niveau ou coefficient hierarchique supérieur. 21 | 22 | 23 | 24 | }> 25 | Les groupes ne comportant pas{" "} 26 | au moins 10 femmes et 10 hommes ne sont pas retenus 27 | pour le calcul. 28 | 29 | 30 | }> 31 | Si aucune promotion n'est intervenue au cours de la période de 32 | référence, l’indicateur n’est pas calculable. 33 | 34 | 35 | }> 36 | Si le total des effectifs retenus est inférieur à 40% des effectifs pris 37 | en compte pour le calcul des indicateurs, l’indicateur n’est pas 38 | calculable. 39 | 40 |
41 | ); 42 | } 43 | 44 | export default FAQIndicateur3Steps; 45 | -------------------------------------------------------------------------------- /packages/kinto/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [2.0.0](https://github.com/SocialGouv/egapro/compare/v1.4.0...v2.0.0) (2019-11-04) 7 | 8 | **Note:** Version bump only for package kinto 9 | 10 | 11 | 12 | 13 | 14 | ## [1.4.3](https://github.com/SocialGouv/egapro/compare/v1.4.0...v1.4.3) (2019-11-04) 15 | 16 | **Note:** Version bump only for package kinto 17 | 18 | 19 | 20 | 21 | 22 | # [1.4.0](https://github.com/SocialGouv/egapro/compare/v1.2.4...v1.4.0) (2019-11-04) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency dotenv to v8.2.0 ([#193](https://github.com/SocialGouv/egapro/issues/193)) ([e1b4f0f](https://github.com/SocialGouv/egapro/commit/e1b4f0f)) 28 | 29 | 30 | 31 | 32 | 33 | ## [1.2.4](https://github.com/SocialGouv/egapro/compare/v1.2.3...v1.2.4) (2019-10-08) 34 | 35 | **Note:** Version bump only for package kinto 36 | 37 | 38 | 39 | 40 | 41 | # 1.1.0 (2019-07-03) 42 | 43 | 44 | ### Features 45 | 46 | * **.env:** use .env file at the root directory ([57d5b6f](https://github.com/SocialGouv/egapro/commit/57d5b6f)) 47 | * **api:** change update api signature ([fbe44d4](https://github.com/SocialGouv/egapro/commit/fbe44d4)) 48 | * **indicator-data:** add model, repo, service, api ([fbcc539](https://github.com/SocialGouv/egapro/commit/fbcc539)) 49 | * **kinto:** add kinto package ([79523cb](https://github.com/SocialGouv/egapro/commit/79523cb)) 50 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQIndicateur4Steps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconPeople, IconGrow } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQIndicateur4Steps() { 9 | return ( 10 | 11 | }> 12 | Seules les salariées qui sont rentrées de congé maternité (ou 13 | d’adoption) durant la période de référence sont prises en considération. 14 | 15 | 16 | }> 17 | Sont considérées comme augmentées toutes salariées{" "} 18 | revenues de congé maternité pendant l'année de 19 | référence et ayant bénéficié d'une augmentation{" "} 20 | (générale ou individuelle){" "} 21 | à leur retour avant la fin de cette même période. 22 | 23 | 24 | }> 25 | Si il n'y a eu aucun retour de congé maternité (ou adoption) au cours de 26 | la période de référence, l’indicateur n’est pas calculable. 27 | 28 | 29 | }> 30 | S'il n'y a eu aucune augmentation (individuelle ou collective) 31 | au cours des congés maternité, l’indicateur n’est pas calculable. 32 | 33 | 34 | ); 35 | } 36 | 37 | export default FAQIndicateur4Steps; 38 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "2.3.0", 4 | "main": "index.js", 5 | "author": "Incubateur des Ministères Sociaux (https://incubateur.social.gouv.fr)", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "start": "nodemon --watch 'src/**/*' -e ts --exec ts-node src/server.ts", 10 | "build": "tsc", 11 | "lint": "tslint -p tsconfig.json -t stylish", 12 | "test": "jest" 13 | }, 14 | "dependencies": { 15 | "@koa/cors": "3.0.0", 16 | "@sentry/node": "5.10.2", 17 | "@types/btoa": "1.2.3", 18 | "btoa": "1.2.1", 19 | "dotenv": "8.2.0", 20 | "koa": "2.11.0", 21 | "koa-bodyparser": "4.2.1", 22 | "koa-router": "7.4.0", 23 | "node-fetch": "2.6.0", 24 | "nodemailer": "6.4.2", 25 | "pino": "5.15.0" 26 | }, 27 | "devDependencies": { 28 | "@sentry/types": "5.10.0", 29 | "@socialgouv/tslint-config-recommended": "0.11.1", 30 | "@types/btoa": "1.2.3", 31 | "@types/dotenv": "6.1.1", 32 | "@types/jest": "24.0.24", 33 | "@types/koa": "2.11.0", 34 | "@types/koa-bodyparser": "4.3.0", 35 | "@types/koa-router": "7.0.42", 36 | "@types/koa__cors": "2.2.3", 37 | "@types/node": "12.12.21", 38 | "@types/node-fetch": "2.5.4", 39 | "@types/nodemailer": "6.4.0", 40 | "@types/pino": "5.15.0", 41 | "jest": "24.9.0", 42 | "nodemon": "2.0.2", 43 | "prettier": "1.19.1", 44 | "ts-jest": "24.2.0", 45 | "ts-node": "8.5.4", 46 | "tslint": "5.20.1", 47 | "typescript": "3.7.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/app/src/components/ScrollContext.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { 4 | useRef, 5 | useContext, 6 | useCallback, 7 | createContext, 8 | ReactNode 9 | } from "react"; 10 | 11 | // Context 12 | 13 | export const ScrollContext = createContext({ 14 | scrollTo: (coord: number) => {} 15 | }); 16 | 17 | // Provider 18 | 19 | function ScrollProvider({ 20 | children, 21 | style 22 | }: { 23 | children: ReactNode; 24 | style: any; 25 | }) { 26 | const scrollRef = useRef(null); 27 | const scrollEl = scrollRef.current; 28 | 29 | const scrollTo = useCallback( 30 | (coord: number) => { 31 | if (scrollEl) { 32 | if (scrollEl.scrollTo) { 33 | scrollEl.scrollTo(coord, 0); 34 | } else { 35 | scrollEl.scrollTop = coord; 36 | } 37 | } 38 | }, 39 | [scrollEl] 40 | ); 41 | 42 | return ( 43 | 44 |
45 | {children} 46 |
47 |
48 | ); 49 | } 50 | 51 | const styles = { 52 | scroll: css({ 53 | overflowY: "auto", 54 | WebkitOverflowScrolling: "touch", 55 | "@media print": { 56 | overflow: "visible" 57 | } 58 | }) 59 | }; 60 | 61 | export default ScrollProvider; 62 | 63 | // Consumer 64 | 65 | const useScrollContext = () => useContext(ScrollContext); 66 | 67 | export const useScrollTo = () => { 68 | const { scrollTo } = useScrollContext(); 69 | return scrollTo; 70 | }; 71 | -------------------------------------------------------------------------------- /k8s/app/.deploy-egapro-app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | .deploy-egapro-app-k8s-dev: 3 | image: 4 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 5 | entrypoint: [""] 6 | before_script: 7 | - /apps/create-kubeconfig.sh 8 | - HASH_BRANCH_NAME=$(printf "$CI_COMMIT_REF_NAME" | sha1sum | cut -c1-5) 9 | - export HASH_BRANCH_INGRESS=$HASH_BRANCH_NAME- 10 | - export HASH_BRANCH_NAME=-$HASH_BRANCH_NAME 11 | - envsubst < k8s/app/deployment.yml > k8s/app/deployment-egapro.yml 12 | - envsubst < k8s/app/service.yml > k8s/app/service-egapro.yml 13 | - envsubst < k8s/app/ingress.yml > k8s/app/ingress-egapro.yml 14 | script: 15 | - kubectl apply -f k8s/app/deployment-egapro.yml 16 | - kubectl apply -f k8s/app/service-egapro.yml 17 | - kubectl apply -f k8s/app/ingress-egapro.yml 18 | allow_failure: false 19 | 20 | .deploy-egapro-app-k8s-prod: 21 | image: 22 | name: $CI_REGISTRY/$IMAGE_INFRA_BASE_NAME/docker-kube:latest 23 | entrypoint: [""] 24 | before_script: 25 | - /apps/create-kubeconfig.sh 26 | - envsubst < k8s/app/deployment-prod.yml > k8s/app/deployment-egapro.yml 27 | - envsubst < k8s/app/service.yml > k8s/app/service-egapro.yml 28 | - envsubst < k8s/app/ingress-prod.yml > k8s/app/ingress-prod-egapro.yml 29 | - envsubst < k8s/certificate/certificate.yml > k8s/certificate/certificate-egapro.yml 30 | script: 31 | - kubectl apply -f k8s/app/deployment-egapro.yml 32 | - kubectl apply -f k8s/app/service-egapro.yml 33 | - kubectl apply -f k8s/app/ingress-prod-egapro.yml 34 | - kubectl apply -f k8s/certificate/certificate-egapro.yml 35 | allow_failure: false 36 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components/FAQSectionRow.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import globalStyles from "../../../utils/globalStyles"; 6 | 7 | interface Props { 8 | section: string; 9 | title: string; 10 | detail: string; 11 | } 12 | 13 | function FAQSectionRow({ section, title, detail }: Props) { 14 | return ( 15 | 16 |
17 |
18 | {title} 19 | 20 |
21 | {detail} 22 |
23 | 24 | ); 25 | } 26 | 27 | const styles = { 28 | container: css({ 29 | marginTop: 14, 30 | marginBottom: 14 31 | }), 32 | row: css({ 33 | display: "flex", 34 | flexDirection: "row", 35 | alignItems: "flex-end", 36 | justifyContent: "space-between", 37 | marginBottom: 6 38 | }), 39 | title: css({ 40 | fontSize: 12, 41 | fontWeight: "bold", 42 | lineHeight: "15px", 43 | color: globalStyles.colors.primary, 44 | textTransform: "uppercase" 45 | }), 46 | chevron: css({ 47 | marginLeft: 14, 48 | lineHeight: "15px", 49 | color: globalStyles.colors.primary 50 | }), 51 | detail: css({ 52 | fontSize: 14, 53 | lineHeight: "17px" 54 | }), 55 | 56 | link: css({ 57 | color: globalStyles.colors.default, 58 | textDecoration: "none" 59 | }) 60 | }; 61 | 62 | export default FAQSectionRow; 63 | -------------------------------------------------------------------------------- /packages/kinto/src/kinto-api.js: -------------------------------------------------------------------------------- 1 | const configs = require("./config"); 2 | const btoa = require('btoa'); 3 | 4 | const kintoURL = configs.kintoURL; 5 | const _account = (name) => `${kintoURL}/accounts/${name}`; 6 | const _buckets = () => `${kintoURL}/buckets`; 7 | const _bucket = (bucketName) => _buckets() + `/${bucketName}`; 8 | const _collections = (bucketName) => _bucket(bucketName) + `/collections`; 9 | const _requestOptions = (method, authorization, body) => { 10 | return { 11 | method: method, 12 | headers: header(authorization), 13 | body: JSON.stringify(body) 14 | } 15 | } 16 | 17 | const header = (authorisation) => { 18 | const header = new Headers(); 19 | header.set("Content-Type", "application/json"); 20 | if (authorisation) { 21 | header.set("Authorization", 'Basic ' + btoa(`${configs.adminLogin}:${configs.adminPassword}`)); 22 | } 23 | return header; 24 | } 25 | 26 | const api = async (url, options) => { 27 | const response = await fetch(url, options); 28 | return response.json(); 29 | } 30 | 31 | module.exports.createAdmin = async function (login, password) { 32 | const body = { data: { password: password } }; 33 | return api(_account(login), _requestOptions('PUT', false, body)); 34 | } 35 | 36 | module.exports.createBucket = async function (name) { 37 | const body = { data: { id: name } }; 38 | return api(_buckets(), _requestOptions('POST', true, body)); 39 | } 40 | 41 | module.exports.createCollection = async function (bucket, collection) { 42 | const body = { data: { id: collection } }; 43 | return api(_collections(bucket), _requestOptions('POST', true, body)); 44 | } -------------------------------------------------------------------------------- /packages/app/src/views/Informations/InformationsResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState, TrancheEffectifs } from "../../globals"; 5 | 6 | import ResultBubble from "../../components/ResultBubble"; 7 | import ActionLink from "../../components/ActionLink"; 8 | 9 | interface Props { 10 | nomEntreprise: string; 11 | trancheEffectifs: TrancheEffectifs; 12 | debutPeriodeReference: string; 13 | finPeriodeReference: string; 14 | validateInformations: (valid: FormState) => void; 15 | } 16 | 17 | function InformationsResult({ 18 | nomEntreprise, 19 | trancheEffectifs, 20 | debutPeriodeReference, 21 | finPeriodeReference, 22 | validateInformations 23 | }: Props) { 24 | return ( 25 |
26 | 34 | 35 |

36 | validateInformations("None")}> 37 | modifier les données saisies 38 | 39 |

40 |
41 | ); 42 | } 43 | 44 | const styles = { 45 | container: css({ 46 | maxWidth: 250, 47 | marginTop: 64 48 | }), 49 | edit: css({ 50 | marginTop: 14, 51 | marginBottom: 14, 52 | textAlign: "center" 53 | }) 54 | }; 55 | 56 | export default InformationsResult; 57 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-detail-calcul/FAQIndicateur4DetailCalcul.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconText } from "../../../components/Icons"; 6 | 7 | import FAQStep from "../components/FAQStep"; 8 | import FAQCalculScale from "../components/FAQCalculScale"; 9 | import FAQTitle3 from "../components/FAQTitle3"; 10 | 11 | function FAQIndicateur4DetailCalcul() { 12 | return ( 13 | 14 | Calculer l’indicateur 15 | 16 | 1}> 17 | L’indicateur correspond au ratio entre le nombre de salariées 18 | revenues de congé maternité ou d’adoption pendant la période de 19 | référence et ayant bénéficié d’une augmentation, avant la fin de 20 | celle-ci, si des augmentations ont eu lieu pendant leur congé, 21 | d’une part; et, d’autre part, le nombre de salariés revenus, 22 | pendant la période de référence, de congé maternité ou d’adoption, 23 | durant lequel il y a eu des augmentations salariales 24 | 25 | 26 |
27 | Appliquer le barème pour obtenir votre note 28 | 29 | 35 |
36 |
37 | ); 38 | } 39 | 40 | const styles = { 41 | content: css({ 42 | marginTop: 30 43 | }) 44 | }; 45 | 46 | export default FAQIndicateur4DetailCalcul; 47 | -------------------------------------------------------------------------------- /packages/app/src/components/FormSubmit.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import globalStyles from "../utils/globalStyles"; 5 | 6 | import ButtonSubmit from "./ButtonSubmit"; 7 | import { IconWarning } from "./Icons"; 8 | 9 | interface Props { 10 | submitFailed: boolean; 11 | hasValidationErrors: boolean; 12 | errorMessage?: string; 13 | loading?: boolean; 14 | } 15 | 16 | function FormSubmit({ 17 | submitFailed, 18 | hasValidationErrors, 19 | errorMessage, 20 | loading = false 21 | }: Props) { 22 | return ( 23 |
24 | 30 | {errorMessage && submitFailed && hasValidationErrors && ( 31 |
32 |
33 | 34 |
35 |

{errorMessage}

36 |
37 | )} 38 |
39 | ); 40 | } 41 | 42 | const styles = { 43 | container: css({ 44 | display: "flex", 45 | flexDirection: "column", 46 | alignItems: "flex-start" 47 | }), 48 | error: css({ 49 | display: "flex", 50 | alignItems: "center", 51 | marginTop: 4, 52 | padding: "8px 12px", 53 | backgroundColor: "white", 54 | border: `solid ${globalStyles.colors.error} 1px`, 55 | borderRadius: 5, 56 | color: globalStyles.colors.error, 57 | fontSize: 12 58 | }), 59 | icon: css({ 60 | height: 20, 61 | marginRight: 10 62 | }) 63 | }; 64 | 65 | export default FormSubmit; 66 | -------------------------------------------------------------------------------- /packages/app/src/components/InfoBloc.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | import { IconWarning, IconCircleCross } from "./Icons"; 8 | import { useColumnsWidth, useLayoutType } from "./GridContext"; 9 | 10 | interface Props { 11 | title: string; 12 | text?: ReactNode; 13 | icon?: "warning" | "cross" | null; 14 | } 15 | 16 | function InfoBloc({ title, text, icon = "warning" }: Props) { 17 | const layoutType = useLayoutType(); 18 | const width = useColumnsWidth(layoutType === "desktop" ? 6 : 7); 19 | return ( 20 |
21 | {icon === null ? null : ( 22 |
23 | {icon === "cross" ? : } 24 |
25 | )} 26 | 27 |
28 |

{title}

29 | {text &&

{text}

} 30 |
31 |
32 | ); 33 | } 34 | 35 | const styles = { 36 | bloc: css({ 37 | padding: "12px 18px", 38 | display: "flex", 39 | alignItems: "center", 40 | border: `2px solid ${globalStyles.colors.primary}`, 41 | borderRadius: 5 42 | }), 43 | blocTitle: css({ 44 | fontSize: 18, 45 | lineHeight: "22px", 46 | color: globalStyles.colors.primary 47 | }), 48 | blocIcon: { 49 | marginRight: 22, 50 | color: globalStyles.colors.primary 51 | }, 52 | blocText: css({ 53 | marginTop: 4, 54 | fontSize: 14, 55 | lineHeight: "17px", 56 | color: globalStyles.colors.primary 57 | }) 58 | }; 59 | 60 | export default InfoBloc; 61 | -------------------------------------------------------------------------------- /packages/api/src/configuration/configs.ts: -------------------------------------------------------------------------------- 1 | const asString = ( 2 | env: typeof process.env, 3 | arg: string, 4 | defaultValue: string 5 | ): string => { 6 | const res = env[arg]; 7 | if (!res) { 8 | return defaultValue; 9 | } 10 | return res; 11 | }; 12 | 13 | const asNumber = ( 14 | env: typeof process.env, 15 | arg: string, 16 | defaultValue: number 17 | ): number => { 18 | const res = env[arg]; 19 | if (!res) { 20 | return defaultValue; 21 | } 22 | return Number.parseInt(res, 10); 23 | }; 24 | 25 | const asBoolean = ( 26 | env: typeof process.env, 27 | arg: string, 28 | defaultValue: boolean 29 | ): boolean => { 30 | const res = env[arg]; 31 | if (!res) { 32 | return defaultValue; 33 | } 34 | return "true" === res ? true : false; 35 | }; 36 | 37 | export const getConfiguration = (env: typeof process.env) => ({ 38 | apiPort: asNumber(env, "API_PORT", 4000), 39 | apiSentryDsn: asString(env, "API_SENTRY_DSN", ""), 40 | apiSentryEnvironment: asString(env, "API_SENTRY_ENVIRONMENT", "development"), 41 | 42 | kintoBucket: asString(env, "KINTO_BUCKET", "egapro"), 43 | kintoLogin: asString(env, "KINTO_LOGIN", "admin"), 44 | kintoPassword: asString(env, "KINTO_PASSWORD", "passw0rd"), 45 | kintoURL: `http://${asString(env, "KINTO_SERVER", "localhost")}:8888/v1`, 46 | 47 | mailFrom: asString( 48 | env, 49 | "MAIL_FROM", 50 | "Index EgaPro " 51 | ), 52 | mailHost: asString(env, `MAIL_HOST`, ""), 53 | mailPassword: asString(env, `MAIL_PASSWORD`, ""), 54 | mailPort: asNumber(env, `MAIL_PORT`, 465), 55 | mailUseTLS: asBoolean(env, `MAIL_USE_TLS`, true), 56 | mailUsername: asString(env, `MAIL_USERNAME`, "") 57 | }); 58 | -------------------------------------------------------------------------------- /packages/api/src/service/email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { createTransport, SentMessageInfo } from "nodemailer"; 2 | import { configuration } from "../../configuration"; 3 | import { logger } from "../../util"; 4 | 5 | export interface EmailAddress { 6 | email: string; 7 | name: string; 8 | } 9 | 10 | export interface Email { 11 | to: EmailAddress[]; 12 | bcc: EmailAddress[]; 13 | cci: EmailAddress[]; 14 | subject: string; 15 | bodyText: string; 16 | } 17 | 18 | const transporter = createTransport({ 19 | host: configuration.mailHost, 20 | port: configuration.mailPort, 21 | secure: configuration.mailUseTLS, // true for 465, false for other ports 22 | // tslint:disable-next-line: object-literal-sort-keys 23 | auth: { 24 | user: configuration.mailUsername, 25 | // tslint:disable-next-line: object-literal-sort-keys 26 | pass: configuration.mailPassword 27 | } 28 | }); 29 | 30 | export interface EmailService { 31 | sendEmail: (email: Email) => Promise; 32 | } 33 | 34 | // https://github.com/nodemailer/nodemailer/blob/master/examples/sendmail.js 35 | export const emailService: EmailService = { 36 | sendEmail: (email: Email) => { 37 | logger.info(`[EmailService.sendEmail] subject ${email.subject}`); 38 | const message = { 39 | from: configuration.mailFrom, 40 | to: email.to.map((r: EmailAddress) => `${r.name} <${r.email}>`).join(","), 41 | // tslint:disable-next-line: object-literal-sort-keys 42 | bcc: email.bcc 43 | .map((r: EmailAddress) => `${r.name} <${r.email}>`) 44 | .join(","), 45 | subject: email.subject, 46 | text: email.bodyText 47 | }; 48 | return transporter.sendMail(message); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/FAQSearch.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { useState, ReactNode } from "react"; 4 | 5 | import FAQSearchBox from "./components/FAQSearchBox"; 6 | import FAQQuestionRow from "./components/FAQQuestionRow"; 7 | import FAQTitle2 from "./components/FAQTitle2"; 8 | 9 | import faqDataFuse from "./utils/faqFuse"; 10 | 11 | interface Props { 12 | children: ReactNode; 13 | } 14 | 15 | function FAQSearch({ children }: Props) { 16 | const [searchTerm, setSearchTerm] = useState(""); 17 | 18 | const fuseResults = searchTerm !== "" ? faqDataFuse.search(searchTerm) : null; 19 | 20 | return ( 21 |
22 | 23 | 24 | {searchTerm === "" ? ( 25 | children 26 | ) : ( 27 |
28 | {fuseResults ? ( 29 | fuseResults.map(({ item: { part, title, index, question } }) => ( 30 |
31 | {title} 32 | 33 |
34 | )) 35 | ) : ( 36 |

Pas de résultat

37 | )} 38 |
39 | )} 40 |
41 | ); 42 | } 43 | 44 | const styles = { 45 | container: css({ 46 | display: "flex", 47 | flexDirection: "column" 48 | }), 49 | content: css({ 50 | marginTop: 28, 51 | marginBottom: 14, 52 | display: "flex", 53 | flexDirection: "column" 54 | }), 55 | part: css({ 56 | marginTop: 14 57 | }) 58 | }; 59 | 60 | export default FAQSearch; 61 | -------------------------------------------------------------------------------- /k8s/kinto/deployment-prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: kinto${HASH_BRANCH_NAME} 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | app: kinto${HASH_BRANCH_NAME} 12 | branch: egapro${HASH_BRANCH_NAME} 13 | spec: 14 | containers: 15 | - name: kinto 16 | image: kinto/kinto-server 17 | imagePullPolicy: "IfNotPresent" 18 | ports: 19 | - containerPort: 8888 20 | env: 21 | - name: KINTO_CACHE_BACKEND 22 | value: "kinto.core.cache.memcached" 23 | - name: KINTO_CACHE_HOSTS 24 | value: "cache${HASH_BRANCH_NAME}:${CACHE_PORT} cache${HASH_BRANCH_NAME}:${CACHE_PORT}" 25 | - name: KINTO_STORAGE_BACKEND 26 | value: "kinto.core.storage.postgresql" 27 | - name: KINTO_STORAGE_URL 28 | value: "postgresql://${POSTGRES_API_USER}:${POSTGRES_API_USER_PASSWORD}@${POSTGRES_HOST}/postgres" 29 | - name: KINTO_PERMISSION_BACKEND 30 | value: "kinto.core.permission.postgresql" 31 | - name: KINTO_PERMISSION_URL 32 | value: "postgresql://${POSTGRES_API_USER}:${POSTGRES_API_USER_PASSWORD}@${POSTGRES_HOST}/postgres" 33 | initContainers: 34 | - name: wait-for-postgres 35 | image: postgres:11-alpine 36 | imagePullPolicy: Always 37 | command: 38 | - sh 39 | - -c 40 | - | 41 | retry=120; # 5s * (12 * 10) = 10min 42 | while ! pg_isready -h ${POSTGRES_HOST} > /dev/null 2> /dev/null && [[ $(( retry-- )) -gt 0 ]]; 43 | do 44 | echo "Waiting for Postgres to go Green ($(( retry )))" ; sleep 5s ; done ; 45 | echo Ready; 46 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.2" 3 | 4 | services: 5 | egapro-frontend: 6 | build: 7 | context: . 8 | dockerfile: ./packages/app/Dockerfile 9 | env_file: .env 10 | environment: 11 | REACT_APP_API_URL: http://egapro-api:4000 12 | depends_on: 13 | - egapro-api 14 | ports: 15 | - 8080:9000 16 | links: 17 | - egapro-api 18 | 19 | egapro-api: 20 | build: 21 | context: . 22 | dockerfile: ./packages/api/Dockerfile 23 | env_file: .env 24 | environment: 25 | KINTO_SERVER: kinto-server 26 | depends_on: 27 | - init-kinto 28 | links: 29 | - kinto-server 30 | 31 | db: 32 | image: postgres 33 | environment: 34 | POSTGRES_USER: postgres 35 | POSTGRES_PASSWORD: postgres 36 | volumes: 37 | - egapro-pgdata:/var/lib/postgresql/data 38 | 39 | cache: 40 | image: library/memcached 41 | 42 | kinto-server: 43 | image: kinto/kinto-server 44 | links: 45 | - db 46 | - cache 47 | ports: 48 | - "8888:8888" 49 | environment: 50 | KINTO_CACHE_BACKEND: kinto.core.cache.memcached 51 | KINTO_CACHE_HOSTS: cache:11211 cache:11212 52 | KINTO_STORAGE_BACKEND: kinto.core.storage.postgresql 53 | KINTO_STORAGE_URL: postgresql://postgres:postgres@db/postgres 54 | KINTO_PERMISSION_BACKEND: kinto.core.permission.postgresql 55 | KINTO_PERMISSION_URL: postgresql://postgres:postgres@db/postgres 56 | 57 | init-kinto: 58 | build: 59 | context: ./packages/kinto/ 60 | env_file: .env 61 | links: 62 | - kinto-server 63 | - db 64 | - cache 65 | depends_on: 66 | - db 67 | - kinto-server 68 | - cache 69 | 70 | volumes: 71 | egapro-pgdata: 72 | -------------------------------------------------------------------------------- /k8s/kinto/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: kinto${HASH_BRANCH_NAME} 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | app: kinto${HASH_BRANCH_NAME} 12 | branch: egapro${HASH_BRANCH_NAME} 13 | spec: 14 | containers: 15 | - name: kinto 16 | image: kinto/kinto-server:13.1.1 17 | imagePullPolicy: "IfNotPresent" 18 | ports: 19 | - containerPort: 8888 20 | env: 21 | - name: KINTO_CACHE_BACKEND 22 | value: "kinto.core.cache.memcached" 23 | - name: KINTO_CACHE_HOSTS 24 | value: "cache${HASH_BRANCH_NAME}:${CACHE_PORT} cache${HASH_BRANCH_NAME}:${CACHE_PORT}" 25 | - name: KINTO_STORAGE_BACKEND 26 | value: "kinto.core.storage.postgresql" 27 | - name: KINTO_STORAGE_URL 28 | value: "postgresql://${POSTGRES_API_USER}:${POSTGRES_API_USER_PASSWORD}@postgres${HASH_BRANCH_NAME}/postgres" 29 | - name: KINTO_PERMISSION_BACKEND 30 | value: "kinto.core.permission.postgresql" 31 | - name: KINTO_PERMISSION_URL 32 | value: "postgresql://${POSTGRES_API_USER}:${POSTGRES_API_USER_PASSWORD}@postgres${HASH_BRANCH_NAME}/postgres" 33 | initContainers: 34 | - name: wait-for-postgres 35 | image: postgres:11-alpine 36 | imagePullPolicy: Always 37 | command: 38 | - sh 39 | - -c 40 | - | 41 | retry=120; # 5s * (12 * 10) = 10min 42 | while ! pg_isready -h postgres${HASH_BRANCH_NAME} > /dev/null 2> /dev/null && [[ $(( retry-- )) -gt 0 ]]; 43 | do 44 | echo "Waiting for Postgres to go Green ($(( retry )))" ; sleep 5s ; done ; 45 | echo Ready; 46 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQIndicateur2Steps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp, IconMoney, IconGrow } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQIndicateur2Steps() { 9 | return ( 10 | 11 | 12 | Indicateur concernant les entreprises de plus de 250 salariés 13 | 14 |
15 |
16 | 17 | }> 18 | La notion d' 19 | 20 | augmentation individuelle correspond à une augmentation individuelle 21 | du salaire de base du salarié concerné. 22 | 23 | 24 | 25 | }> 26 | La notion d’augmentation individuelle pour le calcul de cet indicateur 27 | exclut les augmentations de salaires liées à une promotion. 28 | 29 | 30 | }> 31 | Les groupes ne comportant pas{" "} 32 | au moins 10 femmes et 10 hommes ne sont pas retenus 33 | pour le calcul. 34 | 35 | 36 | }> 37 | Si aucune augmentation individuelle n'est intervenue au cours de la 38 | période de référence, l’indicateur n’est pas calculable. 39 | 40 | 41 | }> 42 | Si le total des effectifs retenus est inférieur à 40% des effectifs pris 43 | en compte pour le calcul des indicateurs, l'indicateur n'est pas 44 | calculable. 45 | 46 |
47 | ); 48 | } 49 | 50 | export default FAQIndicateur2Steps; 51 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useCallback } from "react"; 2 | import { Router } from "react-router-dom"; 3 | import ReactPiwik from "react-piwik"; 4 | import { createBrowserHistory } from "history"; 5 | 6 | import { ActionType } from "./globals.d"; 7 | import AppReducer from "./AppReducer"; 8 | 9 | import GridProvider from "./components/GridContext"; 10 | import AppLayout from "./containers/AppLayout"; 11 | 12 | const history = createBrowserHistory(); 13 | 14 | const piwik: any = new ReactPiwik({ 15 | url: "matomo.fabrique.social.gouv.fr", 16 | siteId: 11, 17 | trackErrors: true 18 | }); 19 | 20 | // track the initial pageview 21 | ReactPiwik.push(["trackPageView"]); 22 | 23 | const validateActions = [ 24 | "validateEffectif", 25 | "validateIndicateurUnCoefGroup", 26 | "validateIndicateurUnCoefEffectif", 27 | "validateIndicateurUn", 28 | "validateIndicateurDeux", 29 | "validateIndicateurTrois", 30 | "validateIndicateurQuatre", 31 | "validateIndicateurCinq" 32 | ]; 33 | 34 | function App() { 35 | const [state, dispatchReducer] = useReducer(AppReducer, undefined); 36 | 37 | const dispatch = useCallback( 38 | (action: ActionType) => { 39 | if ( 40 | validateActions.includes(action.type) && 41 | // @ts-ignore 42 | action.valid && 43 | // @ts-ignore 44 | action.valid === "Valid" 45 | ) { 46 | ReactPiwik.push(["trackEvent", "validateForm", action.type]); 47 | } 48 | dispatchReducer(action); 49 | }, 50 | [dispatchReducer] 51 | ); 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | 62 | export default App; 63 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur4/IndicateurQuatreResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | 6 | import { displayPercent } from "../../utils/helpers"; 7 | 8 | import ResultBubble from "../../components/ResultBubble"; 9 | import ActionLink from "../../components/ActionLink"; 10 | 11 | interface Props { 12 | indicateurEcartNombreSalarieesAugmentees: number | undefined; 13 | noteIndicateurQuatre: number | undefined; 14 | validateIndicateurQuatre: (valid: FormState) => void; 15 | } 16 | 17 | function IndicateurQuatreResult({ 18 | indicateurEcartNombreSalarieesAugmentees, 19 | noteIndicateurQuatre, 20 | validateIndicateurQuatre 21 | }: Props) { 22 | return ( 23 |
24 | 38 | 39 |

40 | validateIndicateurQuatre("None")}> 41 | modifier les données saisies 42 | 43 |

44 |
45 | ); 46 | } 47 | 48 | const styles = { 49 | container: css({ 50 | maxWidth: 250, 51 | marginTop: 64 52 | }), 53 | edit: css({ 54 | marginTop: 14, 55 | marginBottom: 14, 56 | textAlign: "center" 57 | }) 58 | }; 59 | 60 | export default IndicateurQuatreResult; 61 | -------------------------------------------------------------------------------- /packages/app/src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | import { useColumnsWidth, useLayoutType } from "./GridContext"; 8 | 9 | interface Props { 10 | title: string; 11 | tagline?: string | Array; 12 | children: ReactNode; 13 | } 14 | 15 | function Page({ title, tagline, children }: Props) { 16 | const layoutType = useLayoutType(); 17 | const width = useColumnsWidth(layoutType === "desktop" ? 6 : 7); 18 | return ( 19 |
20 |
21 |

{title}

22 | {tagline && Array.isArray(tagline) ? ( 23 | tagline.map((tl, index) => ( 24 |

25 | {tl} 26 |

27 | )) 28 | ) : ( 29 |

{tagline}

30 | )} 31 |
32 |
33 | {children} 34 |
35 | ); 36 | } 37 | 38 | const styles = { 39 | page: css({ 40 | display: "flex", 41 | flexDirection: "column", 42 | marginRight: globalStyles.grid.gutterWidth, 43 | marginLeft: globalStyles.grid.gutterWidth, 44 | marginBottom: globalStyles.grid.gutterWidth, 45 | "@media print": { 46 | display: "block", 47 | marginRight: 0 48 | } 49 | }), 50 | title: css({ 51 | marginTop: 36, 52 | fontSize: 32, 53 | lineHeight: "39px", 54 | fontWeight: "normal", 55 | marginLeft: 0, 56 | marginRight: 0, 57 | marginBottom: 0 58 | }), 59 | tagline: css({ 60 | marginTop: 12, 61 | fontSize: 14, 62 | lineHeight: "17px" 63 | }), 64 | spacer: css({ 65 | height: 54 66 | }) 67 | }; 68 | 69 | export default Page; 70 | -------------------------------------------------------------------------------- /packages/api/src/repository/kinto-api.ts: -------------------------------------------------------------------------------- 1 | import btoa = require("btoa"); 2 | import fetch, { RequestInit } from "node-fetch"; 3 | import { configuration } from "../configuration"; 4 | import { logger } from "../util"; 5 | 6 | const headers = { 7 | // tslint:disable-next-line: object-literal-key-quotes 8 | Authorization: 9 | "Basic " + 10 | btoa(`${configuration.kintoLogin}:${configuration.kintoPassword}`), 11 | "Content-Type": "application/json" 12 | }; 13 | 14 | const bucket = `${configuration.kintoURL}/buckets/${configuration.kintoBucket}`; 15 | const collections = (name: string) => bucket + `/collections/${name}`; 16 | const records = (name: string) => collections(name) + "/records"; 17 | 18 | const requestOptions = (method: "GET" | "POST" | "PUT", body?: any) => { 19 | const options: any = { 20 | headers, 21 | method 22 | }; 23 | if (body) { 24 | options.body = JSON.stringify(body); 25 | } 26 | return options; 27 | }; 28 | 29 | type CollectionFn = (name: string) => KintoCollection; 30 | 31 | const api = async (url: string, options: RequestInit) => { 32 | logger.info(`[kinto-api] ${url}`); 33 | const response = await fetch(url, options); 34 | return response.json(); 35 | }; 36 | 37 | export const collection: CollectionFn = (name: string) => ({ 38 | add: (record: T) => 39 | api(records(name), requestOptions("POST", { data: record })), 40 | one: (id: string) => api(`${records(name)}/${id}`, requestOptions("GET")), 41 | update: (id: string, record: T) => 42 | api(`${records(name)}/${id}`, requestOptions("PUT", { data: record })) 43 | }); 44 | 45 | export interface KintoCollection { 46 | add: (record: T) => Promise>; 47 | update: (id: string, record: T) => Promise>; 48 | one: (id: string) => Promise>; 49 | } 50 | 51 | export interface KintoResult { 52 | data: T; 53 | } 54 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "2.3.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject", 10 | "functional-tests": "cypress run" 11 | }, 12 | "proxy": "http://localhost:4000", 13 | "dependencies": { 14 | "@emotion/core": "10.0.22", 15 | "@types/react-datepicker": "2.9.5", 16 | "@types/react-text-mask": "5.4.6", 17 | "date-fns": "2.8.1", 18 | "deepmerge": "4.2.2", 19 | "express": "4.17.1", 20 | "final-form": "4.18.6", 21 | "final-form-arrays": "3.0.2", 22 | "final-form-calculate": "1.3.1", 23 | "fuse.js": "3.4.6", 24 | "http-proxy-middleware": "0.20.0", 25 | "normalize.css": "8.0.1", 26 | "react": "16.12.0", 27 | "react-app-polyfill": "1.0.5", 28 | "react-datepicker": "2.10.1", 29 | "react-dom": "16.12.0", 30 | "react-final-form": "6.3.0", 31 | "react-final-form-arrays": "3.1.1", 32 | "react-piwik": "1.8.0", 33 | "react-router-dom": "5.1.2", 34 | "react-scripts": "3.3.0", 35 | "react-text-mask": "5.4.3", 36 | "text-mask-addons": "3.8.0" 37 | }, 38 | "devDependencies": { 39 | "@types/jest": "24.0.24", 40 | "@types/node": "12.12.21", 41 | "@types/react": "16.9.16", 42 | "@types/react-dom": "16.9.4", 43 | "@types/react-router-dom": "5.1.3", 44 | "cypress": "3.8.0", 45 | "typescript": "3.7.4", 46 | "wait-on": "3.3.0" 47 | }, 48 | "eslintConfig": { 49 | "extends": "react-app" 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version", 61 | "ie 11" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQEffectifsSteps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp, IconText, IconPeople } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQEffectifsSteps() { 9 | return ( 10 | 11 | CSP}> 12 | L’effectif de l’entreprise est apprécié sur la période de référence. Il 13 | doit être renseigné par{" "} 14 | catégories socio-professionnelles. 15 | 16 | 17 | }> 18 | Les caractéristiques individuelles (âge, catégorie de poste) sont 19 | appréciées au dernier jour de la période de référence ou dernier jour de 20 | présence du salarié dans l’entreprise. 21 | 22 | 23 | }> 24 |

25 | Ne sont pas pris en compte dans les effectifs : 26 |

27 |
    28 |
  • • les apprentis et les contrats de professionnalisation
  • 29 |
  • 30 | • les salariés mis à la disposition de l'entreprise par une 31 | entreprise extérieure (dont les intérimaires) 32 |
  • 33 |
  • • les expatriés
  • 34 |
  • • les salariés en pré-retraite
  • 35 |
  • 36 | • les salariés absents plus de 6 mois sur la période de référence 37 | (arrêt maladie, congés sans solde, cdd <6mois etc.). 38 |
  • 39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | const styles = { 46 | list: css({ 47 | padding: 0, 48 | margin: 0, 49 | listStyle: "none", 50 | marginTop: 6 51 | }) 52 | }; 53 | 54 | export default FAQEffectifsSteps; 55 | -------------------------------------------------------------------------------- /packages/app/src/views/Effectif/EffectifForm.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { useMemo, useCallback } from "react"; 4 | import { 5 | AppState, 6 | FormState, 7 | GroupTranchesAgesEffectif, 8 | ActionEffectifData 9 | } from "../../globals.d"; 10 | 11 | import { displayNameCategorieSocioPro } from "../../utils/helpers"; 12 | 13 | import { ButtonSimulatorLink } from "../../components/SimulatorLink"; 14 | 15 | import EffectifFormRaw from "./EffectifFormRaw"; 16 | 17 | interface Props { 18 | effectif: AppState["effectif"]; 19 | readOnly: boolean; 20 | updateEffectif: (data: ActionEffectifData) => void; 21 | validateEffectif: (valid: FormState) => void; 22 | } 23 | 24 | function EffectifForm({ 25 | effectif, 26 | readOnly, 27 | updateEffectif, 28 | validateEffectif 29 | }: Props) { 30 | const effectifRaw = useMemo( 31 | () => 32 | effectif.nombreSalaries.map(({ categorieSocioPro, tranchesAges }) => ({ 33 | id: categorieSocioPro, 34 | name: displayNameCategorieSocioPro(categorieSocioPro), 35 | tranchesAges 36 | })), 37 | [effectif] 38 | ); 39 | 40 | const updateEffectifRaw = useCallback( 41 | ( 42 | data: Array<{ 43 | id: any; 44 | name: string; 45 | tranchesAges: Array; 46 | }> 47 | ) => { 48 | const nombreSalaries = data.map(({ id, tranchesAges }) => ({ 49 | categorieSocioPro: id, 50 | tranchesAges 51 | })); 52 | updateEffectif({ nombreSalaries }); 53 | }, 54 | [updateEffectif] 55 | ); 56 | 57 | return ( 58 | } 64 | /> 65 | ); 66 | } 67 | 68 | export default EffectifForm; 69 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur1/IndicateurUnResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | 6 | import { displayPercent, displaySexeSurRepresente } from "../../utils/helpers"; 7 | 8 | import ResultBubble from "../../components/ResultBubble"; 9 | import ActionLink from "../../components/ActionLink"; 10 | 11 | interface Props { 12 | indicateurEcartRemuneration: number | undefined; 13 | indicateurSexeSurRepresente: "hommes" | "femmes" | undefined; 14 | noteIndicateurUn: number | undefined; 15 | validateIndicateurUn: (valid: FormState) => void; 16 | } 17 | 18 | function IndicateurUnResult({ 19 | indicateurEcartRemuneration, 20 | indicateurSexeSurRepresente, 21 | noteIndicateurUn, 22 | validateIndicateurUn 23 | }: Props) { 24 | return ( 25 |
26 | 40 | 41 |

42 | validateIndicateurUn("None")}> 43 | modifier les données saisies 44 | 45 |

46 |
47 | ); 48 | } 49 | 50 | const styles = { 51 | container: css({ 52 | maxWidth: 250, 53 | marginTop: 64 54 | }), 55 | edit: css({ 56 | marginTop: 14, 57 | marginBottom: 14, 58 | textAlign: "center" 59 | }) 60 | }; 61 | 62 | export default IndicateurUnResult; 63 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQIndicateur2et3Steps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp, IconGrow } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQIndicateur2et3Steps() { 9 | return ( 10 | 11 | 12 | Indicateur concernant les entreprises entre 50 et 250 salariés 13 | 14 |
15 |
16 | 17 | }> 18 | La notion d' 19 | 20 | augmentation individuelle correspond à une augmentation individuelle 21 | du salaire de base du salarié concerné. 22 | 23 | 24 | 25 | }> 26 | La notion d’augmentation individuelle pour le calcul de cet indicateur{" "} 27 | inclut les augmentations de salaire liées à une 28 | promotion. 29 | 30 | 31 | }> 32 | L'indicateur est calculé au niveau de l'entreprise, et 33 | non par groupes de salariés. 34 | 35 | 36 | }> 37 |

L’indicateur n’est pas calculable :

38 |
    39 |
  • 40 | • Si aucune augmentation individuelle n'est intervenue au cours de 41 | la période de référence, 42 |
  • 43 |
  • 44 | • Ou si l’effectif pris en compte pour le calcul des indicateurs ne 45 | comporte pas au moins 5 femmes et 5 hommes. 46 |
  • 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | const styles = { 54 | list: css({ 55 | padding: 0, 56 | margin: 0, 57 | listStyle: "none", 58 | marginTop: 6 59 | }) 60 | }; 61 | 62 | export default FAQIndicateur2et3Steps; 63 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/FAQQuestion.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | 5 | import { FAQPartType } from "../../globals.d"; 6 | 7 | import ActionLink from "../../components/ActionLink"; 8 | 9 | import FAQTitle from "./components/FAQTitle"; 10 | 11 | import { faqData } from "../../data/faq"; 12 | 13 | interface Props { 14 | part: FAQPartType; 15 | indexQuestion: number; 16 | history: RouteComponentProps["history"]; 17 | } 18 | 19 | function FAQSection({ part, indexQuestion, history }: Props) { 20 | const faqPart = faqData[part]; 21 | const faqQuestion = faqPart.qr[indexQuestion]; 22 | 23 | return ( 24 |
25 | {faqPart.title} 26 | 27 |
28 |

• {faqQuestion.question}

29 |
30 | {faqQuestion.reponse.map((reponsePara, index) => ( 31 |

32 | {reponsePara} 33 |

34 | ))} 35 |
36 | 37 | history.goBack()}> 38 | ︎retour aux questions 39 | 40 |
41 |
42 | ); 43 | } 44 | 45 | const styles = { 46 | container: css({}), 47 | content: css({ 48 | marginBottom: 14 49 | }), 50 | question: css({ 51 | marginBottom: 12, 52 | fontWeight: "bold", 53 | fontSize: 14, 54 | lineHeight: "17px" 55 | }), 56 | responseBloc: css({ 57 | paddingLeft: 15, 58 | marginBottom: 12 59 | }), 60 | responseRow: css({ 61 | marginBottom: 4, 62 | fontSize: 14, 63 | lineHeight: "17px" 64 | }), 65 | 66 | button: css({ 67 | fontSize: 12, 68 | textDecoration: "none" 69 | }), 70 | buttonIcon: css({ 71 | fontSize: 8, 72 | fontFamily: "Segoe UI Symbol" // fix Edge 73 | }) 74 | }; 75 | 76 | export default FAQSection; 77 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur2et3/IndicateurDeuxTroisResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | import { displaySexeSurRepresente } from "../../utils/helpers"; 6 | import { Result } from "./IndicateurDeuxTrois"; 7 | 8 | import ResultBubble from "../../components/ResultBubble"; 9 | import ActionLink from "../../components/ActionLink"; 10 | 11 | interface Props { 12 | bestResult: Result; 13 | indicateurSexeSurRepresente: "hommes" | "femmes" | undefined; 14 | noteIndicateurDeuxTrois: number | undefined; 15 | correctionMeasure: boolean; 16 | validateIndicateurDeuxTrois: (valid: FormState) => void; 17 | } 18 | 19 | function IndicateurDeuxTroisResult({ 20 | bestResult, 21 | indicateurSexeSurRepresente, 22 | noteIndicateurDeuxTrois, 23 | correctionMeasure, 24 | validateIndicateurDeuxTrois 25 | }: Props) { 26 | return ( 27 |
28 | 45 | 46 |

47 | validateIndicateurDeuxTrois("None")}> 48 | modifier les données saisies 49 | 50 |

51 |
52 | ); 53 | } 54 | 55 | const styles = { 56 | container: css({ 57 | maxWidth: 250, 58 | marginTop: 64 59 | }), 60 | edit: css({ 61 | marginTop: 14, 62 | marginBottom: 14, 63 | textAlign: "center" 64 | }) 65 | }; 66 | 67 | export default IndicateurDeuxTroisResult; 68 | -------------------------------------------------------------------------------- /packages/app/src/utils/calculsEgaProIndex.tsx: -------------------------------------------------------------------------------- 1 | import { TrancheEffectifs } from "../globals"; 2 | 3 | /////////// 4 | // Index // 5 | /////////// 6 | 7 | export const calculNoteIndex = ( 8 | trancheEffectifs: TrancheEffectifs, 9 | noteIndicateurUn: number | undefined, 10 | noteIndicateurDeux: number | undefined, 11 | noteIndicateurTrois: number | undefined, 12 | noteIndicateurDeuxTrois: number | undefined, 13 | noteIndicateurQuatre: number | undefined, 14 | noteIndicateurCinq: number | undefined 15 | ): { noteIndex: number | undefined; totalPointCalculable: number } => { 16 | const noteIndicateurUnPointCalculable = 17 | noteIndicateurUn !== undefined ? 40 : 0; 18 | const noteIndicateurDeuxPointCalculable = 19 | noteIndicateurDeux !== undefined ? 20 : 0; 20 | const noteIndicateurTroisPointCalculable = 21 | noteIndicateurTrois !== undefined ? 15 : 0; 22 | const noteIndicateurDeuxTroisPointCalculable = 23 | noteIndicateurDeuxTrois !== undefined ? 35 : 0; 24 | const noteIndicateurQuatrePointCalculable = 25 | noteIndicateurQuatre !== undefined ? 15 : 0; 26 | const noteIndicateurCinqPointCalculable = 27 | noteIndicateurCinq !== undefined ? 10 : 0; 28 | 29 | const totalPointCalculable = 30 | noteIndicateurUnPointCalculable + 31 | (trancheEffectifs !== "50 à 250" 32 | ? noteIndicateurDeuxPointCalculable + noteIndicateurTroisPointCalculable 33 | : noteIndicateurDeuxTroisPointCalculable) + 34 | noteIndicateurQuatrePointCalculable + 35 | noteIndicateurCinqPointCalculable; 36 | 37 | if (totalPointCalculable < 75) { 38 | return { 39 | noteIndex: undefined, 40 | totalPointCalculable 41 | }; 42 | } 43 | 44 | const totalPoint = 45 | (noteIndicateurUn || 0) + 46 | (trancheEffectifs !== "50 à 250" 47 | ? (noteIndicateurDeux || 0) + (noteIndicateurTrois || 0) 48 | : noteIndicateurDeuxTrois || 0) + 49 | (noteIndicateurQuatre || 0) + 50 | (noteIndicateurCinq || 0); 51 | 52 | const noteIndex = Math.round((totalPoint * 100) / totalPointCalculable); 53 | 54 | return { 55 | noteIndex, 56 | totalPointCalculable 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur2/IndicateurDeuxResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | 6 | import { displayPercent, displaySexeSurRepresente } from "../../utils/helpers"; 7 | 8 | import ResultBubble from "../../components/ResultBubble"; 9 | import ActionLink from "../../components/ActionLink"; 10 | 11 | interface Props { 12 | indicateurEcartAugmentation: number | undefined; 13 | indicateurSexeSurRepresente: "hommes" | "femmes" | undefined; 14 | noteIndicateurDeux: number | undefined; 15 | correctionMeasure: boolean; 16 | validateIndicateurDeux: (valid: FormState) => void; 17 | } 18 | 19 | function IndicateurDeuxResult({ 20 | indicateurEcartAugmentation, 21 | indicateurSexeSurRepresente, 22 | noteIndicateurDeux, 23 | correctionMeasure, 24 | validateIndicateurDeux 25 | }: Props) { 26 | return ( 27 |
28 | 47 | 48 |

49 | validateIndicateurDeux("None")}> 50 | modifier les données saisies 51 | 52 |

53 |
54 | ); 55 | } 56 | 57 | const styles = { 58 | container: css({ 59 | maxWidth: 250, 60 | marginTop: 64 61 | }), 62 | edit: css({ 63 | marginTop: 14, 64 | marginBottom: 14, 65 | textAlign: "center" 66 | }) 67 | }; 68 | 69 | export default IndicateurDeuxResult; 70 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components/FAQSearchBox.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import globalStyles from "../../../utils/globalStyles"; 5 | 6 | import { IconSearch, IconClose } from "../../../components/Icons"; 7 | 8 | interface Props { 9 | searchTerm: string; 10 | setSearchTerm: (searchTerm: string) => void; 11 | } 12 | 13 | function FAQSearchBox({ searchTerm, setSearchTerm }: Props) { 14 | const onChange = (event: any) => setSearchTerm(event.target.value); 15 | return ( 16 |
17 | {searchTerm !== "" ? ( 18 |
setSearchTerm("")} 21 | > 22 | 23 |
24 | ) : ( 25 |
26 | 27 |
28 | )} 29 | 30 | 38 |
39 | ); 40 | } 41 | 42 | const styles = { 43 | container: css({ 44 | position: "relative" 45 | }), 46 | input: css({ 47 | appearance: "none", 48 | height: 52, 49 | paddingLeft: 52, 50 | paddingRight: 26, 51 | border: `solid ${globalStyles.colors.primary} 1px`, 52 | borderRadius: 0, 53 | width: "100%", 54 | fontSize: 14, 55 | fontWeight: "bold", 56 | color: globalStyles.colors.primary, 57 | "::placeholder": { 58 | fontWeight: "normal", 59 | color: globalStyles.colors.primary 60 | }, 61 | "::-webkit-search-cancel-button": { 62 | WebkitAppearance: "none" 63 | } 64 | }), 65 | icon: css({ 66 | height: 52, 67 | width: 52, 68 | position: "absolute", 69 | top: 0, 70 | left: 0, 71 | display: "flex", 72 | alignItems: "center", 73 | justifyContent: "center" 74 | }), 75 | iconEnabled: css({ 76 | cursor: "pointer" 77 | }), 78 | iconDisabled: css({ 79 | pointerEvents: "none" 80 | }) 81 | }; 82 | 83 | export default FAQSearchBox; 84 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur3/IndicateurTroisResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | import { displayPercent, displaySexeSurRepresente } from "../../utils/helpers"; 6 | 7 | import ResultBubble from "../../components/ResultBubble"; 8 | import ActionLink from "../../components/ActionLink"; 9 | 10 | interface Props { 11 | indicateurEcartPromotion: number | undefined; 12 | indicateurSexeSurRepresente: "hommes" | "femmes" | undefined; 13 | noteIndicateurTrois: number | undefined; 14 | correctionMeasure: boolean; 15 | validateIndicateurTrois: (valid: FormState) => void; 16 | } 17 | 18 | function IndicateurTroisResult({ 19 | indicateurEcartPromotion, 20 | indicateurSexeSurRepresente, 21 | noteIndicateurTrois, 22 | correctionMeasure, 23 | validateIndicateurTrois 24 | }: Props) { 25 | return ( 26 |
27 | 47 | 48 |

49 | validateIndicateurTrois("None")}> 50 | modifier les données saisies 51 | 52 |

53 |
54 | ); 55 | } 56 | 57 | const styles = { 58 | container: css({ 59 | maxWidth: 250, 60 | marginTop: 64 61 | }), 62 | edit: css({ 63 | marginTop: 14, 64 | marginBottom: 14, 65 | textAlign: "center" 66 | }) 67 | }; 68 | 69 | export default IndicateurTroisResult; 70 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur1/IndicateurUnCsp/IndicateurUnCsp.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { useCallback } from "react"; 4 | 5 | import { 6 | AppState, 7 | FormState, 8 | ActionType, 9 | ActionIndicateurUnCspData 10 | } from "../../../globals"; 11 | 12 | import calculIndicateurUn from "../../../utils/calculsEgaProIndicateurUn"; 13 | 14 | import LayoutFormAndResult from "../../../components/LayoutFormAndResult"; 15 | 16 | import IndicateurUnCspForm from "./IndicateurUnCspForm"; 17 | import IndicateurUnResult from "../IndicateurUnResult"; 18 | 19 | interface Props { 20 | state: AppState; 21 | dispatch: (action: ActionType) => void; 22 | } 23 | 24 | function IndicateurUnCsp({ state, dispatch }: Props) { 25 | const updateIndicateurUn = useCallback( 26 | (data: ActionIndicateurUnCspData) => 27 | dispatch({ type: "updateIndicateurUnCsp", data }), 28 | [dispatch] 29 | ); 30 | 31 | const validateIndicateurUn = useCallback( 32 | (valid: FormState) => dispatch({ type: "validateIndicateurUn", valid }), 33 | [dispatch] 34 | ); 35 | 36 | const { 37 | effectifEtEcartRemuParTrancheCsp, 38 | indicateurEcartRemuneration, 39 | indicateurSexeSurRepresente, 40 | noteIndicateurUn 41 | } = calculIndicateurUn(state); 42 | 43 | return ( 44 | 53 | } 54 | childrenResult={ 55 | state.indicateurUn.formValidated === "Valid" && ( 56 | 62 | ) 63 | } 64 | /> 65 | ); 66 | } 67 | 68 | export default IndicateurUnCsp; 69 | -------------------------------------------------------------------------------- /k8s/api/deployment-prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Deployment 3 | apiVersion: apps/v1 4 | metadata: 5 | name: egapro-api${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-api${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: egapro-api${HASH_BRANCH_NAME} 14 | template: 15 | metadata: 16 | labels: 17 | app: egapro-api${HASH_BRANCH_NAME} 18 | spec: 19 | containers: 20 | - image: ${EGAPRO_REGISTRY}/api:${IMAGE_TAG} 21 | name: egapro-api 22 | ports: 23 | - containerPort: ${PORT} 24 | env: 25 | - name: API_PORT 26 | value: "${PORT}" 27 | - name: KINTO_SERVER 28 | value: "kinto${HASH_BRANCH_NAME}" 29 | - name: KINTO_BUCKET 30 | value: "egapro" 31 | - name: KINTO_LOGIN 32 | valueFrom: 33 | secretKeyRef: 34 | name: egapro-secret 35 | key: KINTO_ADMIN_LOGIN 36 | - name: KINTO_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: egapro-secret 40 | key: KINTO_ADMIN_PASSWORD 41 | - name: MAIL_USE_TLS 42 | value: "true" 43 | - name: MAIL_PORT 44 | value: "465" 45 | - name: MAIL_FROM 46 | value: "Index EgaPro " 47 | - name: MAIL_HOST 48 | valueFrom: 49 | secretKeyRef: 50 | name: egapro-secret 51 | key: MAIL_HOST 52 | - name: MAIL_USERNAME 53 | valueFrom: 54 | secretKeyRef: 55 | name: egapro-secret 56 | key: MAIL_USERNAME 57 | - name: MAIL_PASSWORD 58 | valueFrom: 59 | secretKeyRef: 60 | name: egapro-secret 61 | key: MAIL_PASSWORD 62 | - name: API_SENTRY_DSN 63 | valueFrom: 64 | secretKeyRef: 65 | name: egapro-secret 66 | key: API_SENTRY_DSN 67 | - name: SENTRY_ENVIRONMENT 68 | value: "production" 69 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur1/IndicateurUnTypeForm.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { useCallback, Fragment } from "react"; 4 | import { Form } from "react-final-form"; 5 | import { ActionIndicateurUnTypeData, ActionType } from "../../globals.d"; 6 | 7 | import { 8 | parseBooleanFormValue, 9 | parseBooleanStateValue 10 | } from "../../utils/formHelpers"; 11 | 12 | import FormAutoSave from "../../components/FormAutoSave"; 13 | import RadiosBoolean from "../../components/RadiosBoolean"; 14 | 15 | interface Props { 16 | csp: boolean; 17 | readOnly: boolean; 18 | dispatch: (action: ActionType) => void; 19 | } 20 | 21 | function IndicateurUnTypeForm({ csp, readOnly, dispatch }: Props) { 22 | const updateIndicateurUnType = useCallback( 23 | (data: ActionIndicateurUnTypeData) => 24 | dispatch({ type: "updateIndicateurUnType", data }), 25 | [dispatch] 26 | ); 27 | 28 | const initialValues = { csp: parseBooleanStateValue(csp) }; 29 | 30 | const saveForm = (formData: any) => { 31 | const { csp: cspFormData } = formData; 32 | 33 | if (cspFormData !== csp) { 34 | updateIndicateurUnType({ csp: parseBooleanFormValue(cspFormData) }); 35 | } 36 | }; 37 | 38 | return ( 39 |
{}} initialValues={initialValues}> 40 | {({ handleSubmit, values }) => ( 41 | 42 | 43 | 49 | je renseigne par{" "} 50 | Catégories Socio-Professionnelles 51 | 52 | } 53 | labelFalse={ 54 | 55 | je renseigne par{" "} 56 | Niveaux ou coefficients hiérarchiques 57 | 58 | } 59 | /> 60 | 61 | )} 62 | 63 | ); 64 | } 65 | 66 | const styles = { 67 | container: css({ 68 | display: "flex", 69 | flexDirection: "column", 70 | marginBottom: 54 71 | }) 72 | }; 73 | 74 | export default IndicateurUnTypeForm; 75 | -------------------------------------------------------------------------------- /packages/app/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import globalStyles from "../utils/globalStyles"; 5 | 6 | import ActivityIndicator from "./ActivityIndicator"; 7 | 8 | interface Props { 9 | label: string; 10 | outline?: boolean; 11 | error?: boolean; 12 | loading?: boolean; 13 | } 14 | 15 | function Button({ 16 | label, 17 | outline = false, 18 | error = false, 19 | loading = false 20 | }: Props) { 21 | return ( 22 |
30 | {label} 31 | {loading && ( 32 |
33 | 34 |
35 | )} 36 |
37 | ); 38 | } 39 | 40 | const styles = { 41 | button: css({ 42 | display: "flex", 43 | alignItems: "center", 44 | justifyContent: "center", 45 | height: 36, 46 | minWidth: 110, 47 | padding: "0 10px", 48 | backgroundColor: globalStyles.colors.primary, 49 | color: "#FFF", 50 | border: `solid ${globalStyles.colors.primary} 1px`, 51 | borderRadius: 5, 52 | cursor: "pointer", 53 | position: "relative", 54 | background: 55 | "linear-gradient(64.86deg, #696CD1 0%, #696CD1 51%, #191A49 100%)", 56 | backgroundSize: "200%", 57 | transition: "background-position 350ms ease-in-out" 58 | }), 59 | buttonHover: css({ 60 | ":hover": { 61 | backgroundPosition: "right center" 62 | } 63 | }), 64 | buttonOutline: css({ 65 | color: globalStyles.colors.primary, 66 | backgroundColor: "#FFF", 67 | background: "none" 68 | }), 69 | buttonError: css({ 70 | color: globalStyles.colors.error, 71 | borderColor: globalStyles.colors.error, 72 | backgroundColor: "#FFF", 73 | background: "none" 74 | }), 75 | 76 | loader: css({ 77 | position: "absolute", 78 | top: 0, 79 | left: 0, 80 | right: 0, 81 | bottom: 0, 82 | 83 | display: "flex", 84 | alignItems: "center", 85 | justifyContent: "center" 86 | }), 87 | 88 | textLoading: css({ 89 | visibility: "hidden" 90 | }) 91 | }; 92 | 93 | export default Button; 94 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-steps/FAQResultatSteps.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp } from "../../../components/Icons"; 6 | import FAQStep from "../components/FAQStep"; 7 | 8 | function FAQResultatSteps() { 9 | return ( 10 | 11 | }> 12 | Les entreprises doivent transmettre leurs résultats au Ministère du Travail 13 | via le formulaire en ligne. 14 | Toutes les informations nécessaires à la transmission se 15 | trouvent sur ce récapitulatif. 16 | 17 | }> 18 | L'index doit être publié sur le site internet de l'entreprise déclarante 19 | ou en l'absence de site internet, communiqué aux salariés. 20 | Les résultats doivent par ailleurs être communiqués au CSE. 21 | 22 | }> 23 |

24 | Si l’entreprise obtient moins de 75 points, elle devra mettre en oeuvre 25 | des mesures de correction lui permettant d’atteindre au moins 75 points 26 | dans un délai 3 ans. 27 |

28 |

29 | Les mesures prises seront définies : 30 |

31 |
    32 |
  • 33 | • dans le cadre de la négociation relative à l’égalité 34 | professionnelle 35 |
  • 36 |
  • 37 | • ou à défaut d’accord, par décision unilatérale de l’employeur et après 38 | consultation du comité social et économique. Cette décision devra être déposée 39 | auprès des services de la Direccte. Elle pourra être intégrée au plan d’action 40 | devant être établi à défaut d’accord relatif à l’égalité professionnelle. 41 |
  • 42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | const styles = { 49 | para: css({ 50 | marginBottom: 6 51 | }), 52 | list: css({ 53 | padding: 0, 54 | margin: 0, 55 | listStyle: "none" 56 | }) 57 | }; 58 | 59 | export default FAQResultatSteps; 60 | -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | 40 | Egapro 41 | 42 | 43 | 44 |
45 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /packages/app/src/utils/calculsEgaProIndicateurQuatre.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from "../globals.d"; 2 | 3 | import { roundDecimal } from "./helpers"; 4 | 5 | ////////////////// 6 | // INDICATEUR 4 // 7 | ////////////////// 8 | 9 | export const calculIndicateurCalculable = ( 10 | presenceCongeMat: boolean, 11 | nombreSalarieesPeriodeAugmentation: number | undefined 12 | ) => { 13 | return ( 14 | presenceCongeMat && 15 | nombreSalarieesPeriodeAugmentation !== undefined && 16 | nombreSalarieesPeriodeAugmentation > 0 17 | ); 18 | }; 19 | 20 | export const calculIndicateurEcartNombreSalarieesAugmentees = ( 21 | indicateurCalculable: boolean, 22 | nombreSalarieesPeriodeAugmentation: number | undefined, 23 | nombreSalarieesAugmentees: number | undefined 24 | ): number | undefined => 25 | indicateurCalculable && 26 | nombreSalarieesPeriodeAugmentation !== undefined && 27 | nombreSalarieesAugmentees !== undefined && 28 | nombreSalarieesPeriodeAugmentation >= nombreSalarieesAugmentees 29 | ? Math.abs( 30 | roundDecimal( 31 | 100 * 32 | (nombreSalarieesAugmentees / nombreSalarieesPeriodeAugmentation), 33 | 3 34 | ) 35 | ) 36 | : undefined; 37 | 38 | // NOTE 39 | export const calculNote = ( 40 | indicateurEcartNombreSalarieesAugmentees: number | undefined 41 | ): number | undefined => 42 | indicateurEcartNombreSalarieesAugmentees !== undefined 43 | ? indicateurEcartNombreSalarieesAugmentees < 100 44 | ? 0 45 | : 15 46 | : undefined; 47 | 48 | ///////// 49 | // ALL // 50 | ///////// 51 | 52 | export default function calculIndicateurQuatre(state: AppState) { 53 | const indicateurCalculable = calculIndicateurCalculable( 54 | state.indicateurQuatre.presenceCongeMat, 55 | state.indicateurQuatre.nombreSalarieesPeriodeAugmentation 56 | ); 57 | 58 | const indicateurEcartNombreSalarieesAugmentees = calculIndicateurEcartNombreSalarieesAugmentees( 59 | indicateurCalculable, 60 | state.indicateurQuatre.nombreSalarieesPeriodeAugmentation, 61 | state.indicateurQuatre.nombreSalarieesAugmentees 62 | ); 63 | 64 | const noteIndicateurQuatre = calculNote( 65 | indicateurEcartNombreSalarieesAugmentees 66 | ); 67 | 68 | return { 69 | indicateurCalculable, 70 | indicateurEcartNombreSalarieesAugmentees, 71 | noteIndicateurQuatre 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /packages/app/src/utils/calculsEgaProIndicateurCinq.tsx: -------------------------------------------------------------------------------- 1 | import { AppState } from "../globals.d"; 2 | 3 | function clamp(num: number, min: number, max: number) { 4 | return Math.min(max, Math.max(min, num)); 5 | } 6 | 7 | ////////////////// 8 | // INDICATEUR 5 // 9 | ////////////////// 10 | 11 | const baremeEcartRemuneration = [0, 0, 5, 5, 10, 10]; 12 | 13 | export const calculIndicateurSexeSousRepresente = ( 14 | nombreSalariesHommes: number | undefined, 15 | nombreSalariesFemmes: number | undefined 16 | ): "hommes" | "femmes" | "egalite" | undefined => 17 | nombreSalariesHommes !== undefined && nombreSalariesFemmes !== undefined 18 | ? nombreSalariesHommes > nombreSalariesFemmes 19 | ? "femmes" 20 | : nombreSalariesHommes < nombreSalariesFemmes 21 | ? "hommes" 22 | : "egalite" 23 | : undefined; 24 | 25 | export const calculIndicateurNombreSalariesSexeSousRepresente = ( 26 | nombreSalariesHommes: number | undefined, 27 | nombreSalariesFemmes: number | undefined 28 | ): number | undefined => 29 | nombreSalariesHommes !== undefined && nombreSalariesFemmes !== undefined 30 | ? Math.min(nombreSalariesHommes, nombreSalariesFemmes) 31 | : undefined; 32 | 33 | // NOTE 34 | export const calculNote = ( 35 | indicateurNombreSalariesSexeSousRepresente: number | undefined 36 | ): number | undefined => 37 | indicateurNombreSalariesSexeSousRepresente !== undefined 38 | ? baremeEcartRemuneration[ 39 | clamp( 40 | indicateurNombreSalariesSexeSousRepresente, 41 | 0, 42 | baremeEcartRemuneration.length - 1 43 | ) 44 | ] 45 | : undefined; 46 | 47 | ///////// 48 | // ALL // 49 | ///////// 50 | 51 | export default function calculIndicateurCinq(state: AppState) { 52 | const indicateurSexeSousRepresente = calculIndicateurSexeSousRepresente( 53 | state.indicateurCinq.nombreSalariesHommes, 54 | state.indicateurCinq.nombreSalariesFemmes 55 | ); 56 | 57 | const indicateurNombreSalariesSexeSousRepresente = calculIndicateurNombreSalariesSexeSousRepresente( 58 | state.indicateurCinq.nombreSalariesHommes, 59 | state.indicateurCinq.nombreSalariesFemmes 60 | ); 61 | 62 | const noteIndicateurCinq = calculNote( 63 | indicateurNombreSalariesSexeSousRepresente 64 | ); 65 | 66 | return { 67 | indicateurSexeSousRepresente, 68 | indicateurNombreSalariesSexeSousRepresente, 69 | noteIndicateurCinq 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dist: xenial 3 | language: minimal 4 | git: 5 | depth: 5 6 | 7 | # 8 | 9 | .env: 10 | github_keys: &github_keys 11 | - secure: "rFKFYTuRoS/F2WsCBKm3NVQAC6Km6ONlKbVKVRKmZWKN09cc9Vi+Hupu9I3lc733qDiPvYADP7/YQIFrpQcfMqTNg6XHnGaW/LhTdnQrr4FGQ+Thafgz9RTxbcICKgGLHXjYT+rRjC+Y8BEATaC9al0ajTVs9OH6oK+4LIvxOhn9OLlvZOy5Xz++7b/1wLno03C9eRbltS6wudhA38SI8RUWNgFyt/sBMdsn2cZYco0Ed/NNUCkyts6xpDj1HH2cFpCx01PuhClFZX8efC5e76IoxLqaQOIAwfeBTtOvfNyd3lhwcqgQhK5dSID6mKcobb+7AtJtqr47RTXVwpjTmIlhMZJTycVJS6H4BIzg3FnHSfgP8YbAgbkWJ4rHVfPnqEt5fT2MN3dqcNc+oPQ2mhj2fZmvhmrTcYX3k/Pgc1sBrLqDcyj4KsBOJDXHKxotNRYlZmyAeaieXNesEnm6tfJp22TzyDXo8PPctnYrr4hbX3vB+802JWxy0rDgwiySQAYn6e4wIKt+yiOik5a7r2UFjOMXpL4Sy1FuKu4uFqJm/8lhysMyavuKKmUAHcLNVbAC9bYVqFDdhu+ENwmU/M0rLpcBUbnNqK8BdP6GdzozGiBt3ZTaLoTOpvNB7QqqOAMEw4nY08fWZAS3Mwr3AsJOS4k86vlW9GGI2XobTg8=" 12 | 13 | # 14 | 15 | .node_stage: &node_stage 16 | language: node_js 17 | node_js: "10" 18 | cache: $HOME/.cache 19 | before_install: 20 | - curl -o- -L https://yarnpkg.com/install.sh | bash 21 | - export PATH="$HOME/.yarn/bin:$PATH" 22 | install: 23 | - yarn --frozen-lockfile 24 | 25 | # 26 | 27 | jobs: 28 | include: 29 | - stage: Build 30 | <<: *node_stage 31 | script: 32 | - yarn build 33 | - yarn lint 34 | - yarn coverage 35 | # The following is for the functional tests 36 | - cp .env.sample .env && yarn start & 37 | - yarn run wait-on http://localhost:4000/api/version 38 | - yarn run wait-on http://localhost:3000 39 | - yarn functional-tests 40 | after_script: 41 | - npx codecov 42 | 43 | # 44 | # 45 | # 46 | 47 | - stage: Release 48 | name: Make a new release 🎉 49 | if: env(RELEASE) 50 | <<: *node_stage 51 | git: 52 | # NOTE(douglasduteil): disable git --depth 53 | # Try to have all the commits for the release Change Log 54 | # see travis-ci/travis-ci#3412 55 | depth: 9999999 # Over 9000 ! 56 | env: *github_keys 57 | before_script: 58 | - git checkout ${TRAVIS_BRANCH} 59 | - git config user.name "Social Groovy Bot" 60 | - git config user.email "45039513+SocialGroovyBot@users.noreply.github.com" 61 | - git remote set-url origin https://${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git 62 | script: 63 | - GH_TOKEN=${GITHUB_TOKEN} yarn lerna version ${LERNA_ARGS:=--yes} 64 | -------------------------------------------------------------------------------- /packages/app/src/utils/api.js: -------------------------------------------------------------------------------- 1 | // When deploying using docker, use the API_URL env variable to proxy to the 2 | // egapro-api image. See the server.js file for the proxy configuration. 3 | // When in development, any unrecognized URL will be proxied to the `proxy` 4 | // entry in the package.json (see 5 | // https://create-react-app.dev/docs/proxying-api-requests-in-development/) 6 | const origin = "/api"; 7 | 8 | const commonHeaders = { 9 | Accept: "application/json" 10 | }; 11 | 12 | const commonContentHeaders = { 13 | ...commonHeaders, 14 | "Content-Type": "application/json" 15 | }; 16 | 17 | //////////// 18 | 19 | class ApiError extends Error { 20 | constructor(response, jsonBody, ...params) { 21 | super(...params); 22 | if (Error.captureStackTrace) { 23 | Error.captureStackTrace(this, ApiError); 24 | } 25 | this.response = response; 26 | this.jsonBody = jsonBody; 27 | } 28 | } 29 | 30 | function checkStatusAndParseJson(response) { 31 | if (response.status === 204) { 32 | return { response }; 33 | } 34 | 35 | let jsonPromise = response.json(); // there's always a body 36 | if (response.status >= 200 && response.status < 300) { 37 | return jsonPromise.then(jsonBody => ({ response, jsonBody })); 38 | } else { 39 | return jsonPromise.then(jsonBody => { 40 | const apiError = new ApiError(response, jsonBody); 41 | return Promise.reject.bind(Promise)(apiError); 42 | }); 43 | } 44 | } 45 | 46 | ///////////// 47 | 48 | function fetchResource(method, pathname, body) { 49 | const requestObj = { 50 | method, 51 | headers: body ? commonContentHeaders : commonHeaders, 52 | body: body ? JSON.stringify(body) : undefined 53 | }; 54 | 55 | return fetch(origin + pathname, requestObj).then(checkStatusAndParseJson); 56 | } 57 | 58 | const getResource = pathname => fetchResource("GET", pathname); 59 | const postResource = (pathname, body) => fetchResource("POST", pathname, body); 60 | const putResource = (pathname, body) => fetchResource("PUT", pathname, body); 61 | 62 | ///////////// 63 | 64 | export const getIndicatorsDatas = id => getResource(`/indicators-datas/${id}`); 65 | 66 | export const postIndicatorsDatas = data => 67 | postResource("/indicators-datas", data); 68 | 69 | export const putIndicatorsDatas = (id, data) => 70 | putResource(`/indicators-datas/${id}`, { id, data }); 71 | 72 | export const sendEmailIndicatorsDatas = (id, email) => 73 | postResource(`/indicators-datas/${id}/emails`, { email }); 74 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur1/IndicateurUnCsp/IndicateurUnCspForm.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { useMemo, useCallback } from "react"; 4 | import { 5 | ActionIndicateurUnCspData, 6 | AppState, 7 | GroupTranchesAgesIndicateurUn, 8 | FormState 9 | } from "../../../globals"; 10 | 11 | import { effectifEtEcartRemuGroupCsp } from "../../../utils/calculsEgaProIndicateurUn"; 12 | 13 | import { ButtonSimulatorLink } from "../../../components/SimulatorLink"; 14 | 15 | import { displayNameCategorieSocioPro } from "../../../utils/helpers"; 16 | 17 | import IndicateurUnFormRaw from "../IndicateurUnFormRaw"; 18 | 19 | interface Props { 20 | state: AppState; 21 | ecartRemuParTrancheAge: Array; 22 | readOnly: boolean; 23 | updateIndicateurUn: (data: ActionIndicateurUnCspData) => void; 24 | validateIndicateurUn: (valid: FormState) => void; 25 | } 26 | 27 | function IndicateurUnCspForm({ 28 | state, 29 | ecartRemuParTrancheAge, 30 | readOnly, 31 | updateIndicateurUn, 32 | validateIndicateurUn 33 | }: Props) { 34 | const ecartRemuParTrancheAgeRaw = useMemo( 35 | () => 36 | ecartRemuParTrancheAge.map(({ categorieSocioPro, ...otherAttr }) => ({ 37 | id: categorieSocioPro, 38 | name: displayNameCategorieSocioPro(categorieSocioPro), 39 | ...otherAttr 40 | })), 41 | [ecartRemuParTrancheAge] 42 | ); 43 | 44 | const updateIndicateurUnRaw = useCallback( 45 | ( 46 | data: Array<{ 47 | id: any; 48 | tranchesAges: Array; 49 | }> 50 | ) => { 51 | const remunerationAnnuelle = data.map(({ id, tranchesAges }) => ({ 52 | categorieSocioPro: id, 53 | tranchesAges 54 | })); 55 | updateIndicateurUn({ remunerationAnnuelle }); 56 | }, 57 | [updateIndicateurUn] 58 | ); 59 | 60 | return ( 61 | 75 | } 76 | /> 77 | ); 78 | } 79 | 80 | export default IndicateurUnCspForm; 81 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur5/IndicateurCinqResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | 6 | import ResultBubble from "../../components/ResultBubble"; 7 | import ActionLink from "../../components/ActionLink"; 8 | 9 | interface Props { 10 | indicateurSexeSousRepresente: "hommes" | "femmes" | "egalite" | undefined; 11 | indicateurNombreSalariesSexeSousRepresente: number | undefined; 12 | noteIndicateurCinq: number | undefined; 13 | validateIndicateurCinq: (valid: FormState) => void; 14 | } 15 | 16 | function IndicateurCinqResult({ 17 | indicateurSexeSousRepresente, 18 | indicateurNombreSalariesSexeSousRepresente, 19 | noteIndicateurCinq, 20 | validateIndicateurCinq 21 | }: Props) { 22 | const firstLineInfo = 23 | indicateurSexeSousRepresente === undefined 24 | ? undefined 25 | : indicateurSexeSousRepresente === "egalite" 26 | ? "les femmes et les hommes sont à égalité" 27 | : indicateurSexeSousRepresente === "hommes" 28 | ? "les femmes sont sur-représentées" 29 | : "les hommes sont sur-représentés"; 30 | return ( 31 |
32 | 53 | 54 |

55 | validateIndicateurCinq("None")}> 56 | modifier les données saisies 57 | 58 |

59 |
60 | ); 61 | } 62 | 63 | const styles = { 64 | container: css({ 65 | maxWidth: 250, 66 | marginTop: 64 67 | }), 68 | edit: css({ 69 | marginTop: 14, 70 | marginBottom: 14, 71 | textAlign: "center" 72 | }) 73 | }; 74 | 75 | export default IndicateurCinqResult; 76 | -------------------------------------------------------------------------------- /k8s/api/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Deployment 3 | apiVersion: apps/v1 4 | metadata: 5 | name: egapro-api${HASH_BRANCH_NAME} 6 | labels: 7 | app: egapro-api${HASH_BRANCH_NAME} 8 | branch: egapro${HASH_BRANCH_NAME} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: egapro-api${HASH_BRANCH_NAME} 14 | template: 15 | metadata: 16 | labels: 17 | app: egapro-api${HASH_BRANCH_NAME} 18 | spec: 19 | containers: 20 | - image: ${EGAPRO_REGISTRY}/api:${IMAGE_TAG} 21 | name: egapro-api 22 | ports: 23 | - containerPort: ${PORT} 24 | env: 25 | - name: API_PORT 26 | value: "${PORT}" 27 | - name: KINTO_SERVER 28 | value: "kinto${HASH_BRANCH_NAME}" 29 | - name: KINTO_BUCKET 30 | value: "egapro" 31 | - name: KINTO_LOGIN 32 | valueFrom: 33 | secretKeyRef: 34 | name: egapro-secret 35 | key: KINTO_ADMIN_LOGIN 36 | - name: KINTO_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: egapro-secret 40 | key: KINTO_ADMIN_PASSWORD 41 | - name: MAIL_USE_TLS 42 | valueFrom: 43 | secretKeyRef: 44 | name: egapro-secret 45 | key: MAIL_USE_TLS 46 | - name: MAIL_PORT 47 | valueFrom: 48 | secretKeyRef: 49 | name: egapro-secret 50 | key: MAIL_PORT 51 | - name: MAIL_FROM 52 | value: "Index EgaPro " 53 | - name: MAIL_HOST 54 | valueFrom: 55 | secretKeyRef: 56 | name: egapro-secret 57 | key: MAIL_HOST 58 | - name: MAIL_USERNAME 59 | valueFrom: 60 | secretKeyRef: 61 | name: egapro-secret 62 | key: MAIL_USERNAME 63 | - name: MAIL_PASSWORD 64 | valueFrom: 65 | secretKeyRef: 66 | name: egapro-secret 67 | key: MAIL_PASSWORD 68 | - name: API_SENTRY_DSN 69 | valueFrom: 70 | secretKeyRef: 71 | name: egapro-secret 72 | key: API_SENTRY_DSN 73 | - name: API_SENTRY_ENVIRONMENT 74 | value: "development" 75 | -------------------------------------------------------------------------------- /packages/app/src/containers/MobileLayout.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { useState } from "react"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | import MobileHome from "../views/MobileHome"; 8 | import FAQ from "../views/FAQ"; 9 | 10 | function MobileLayout() { 11 | const [isMenuOpen, setIsMenuOpen] = useState(false); 12 | const openMenu = () => setIsMenuOpen(true); 13 | const closeMenu = () => setIsMenuOpen(false); 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | const styles = { 27 | mobileLayout: css({ 28 | overflow: "hidden", 29 | position: "fixed", 30 | top: 0, 31 | bottom: 0, 32 | left: 0, 33 | right: 0, 34 | 35 | display: "flex", 36 | flexDirection: "column", 37 | 38 | /* *** /!\ *** */ 39 | flexGrow: 0, 40 | flexShrink: 1, 41 | flexBasis: globalStyles.grid.maxWidth - 375, 42 | minWidth: 0, 43 | /* 44 | Fix issue with IE11 45 | the intention would be something like: 46 | 47 | flexGrow: 1, 48 | flexShrink: 1, 49 | flexBasis: "auto", 50 | maxWidth: globalStyles.grid.maxWidth - 375, 51 | 52 | but the code above is a workaround described here: 53 | https://github.com/philipwalton/flexbugs#flexbug-17 54 | */ 55 | 56 | background: 57 | "linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #FFFFFF 100%), #EFF0FA", 58 | 59 | "@media print": { 60 | display: "block" 61 | } 62 | }), 63 | scroll: css({ 64 | overflowY: "auto", 65 | WebkitOverflowScrolling: "touch", 66 | flexGrow: 1, 67 | flexShrink: 1, 68 | flexBasis: "0%", 69 | position: "relative" 70 | }), 71 | faq: css({ 72 | position: "absolute", 73 | top: 0, 74 | bottom: 0, 75 | left: 0, 76 | right: 0, 77 | display: "flex", 78 | flexDirection: "column", 79 | 80 | visibility: "hidden", 81 | transform: "translate3d(100%, 0, 0)", 82 | transition: "visibility 0ms linear 250ms, transform 250ms ease-in-out", 83 | 84 | boxShadow: "0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)" 85 | }), 86 | faqOpen: css({ 87 | visibility: "visible", 88 | transform: "translate3d(0%, 0, 0)", 89 | transitionDelay: "0ms" 90 | }) 91 | }; 92 | 93 | export default MobileLayout; 94 | -------------------------------------------------------------------------------- /k8s/scripts/delete-k8s-objects.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output 2 | import hashlib 3 | 4 | # This script compares the active remote branches and active k8s tags. 5 | # If a k8s tag doesn't match an active hashed remote branches name's, we delete all the k8s objects with this k8s tag. 6 | 7 | def get_active_branches(): 8 | active_branche_list = [] 9 | raw_active_branche = check_output('git branch -r', shell=True) 10 | for active_branch in raw_active_branche.decode('utf-8').split('\n'): 11 | if active_branch and not active_branch.startswith('*') and active_branch.strip() != 'origin/master' and active_branch.strip() != 'origin/HEAD -> origin/master': 12 | active_branch = active_branch.replace('origin/','').replace('/','-').strip() 13 | active_hash_branch = hashlib.sha1(active_branch.strip().encode()) 14 | active_branche_list.append(active_hash_branch.hexdigest()[:5]) 15 | return active_branche_list 16 | 17 | def get_active_k8s_tags(): 18 | active_k8s_tag_list = [] 19 | raw_k8s_tag_list = check_output("kubectl get deployments -o go-template --template '{{range .items}}{{.metadata.labels.branch}}{{end}}'", shell=True) 20 | for active_k8s_tag in raw_k8s_tag_list.decode('utf-8').replace('','').split('egapro-'): 21 | active_k8s_tag_list.append(active_k8s_tag) 22 | return active_k8s_tag_list 23 | 24 | def delete_k8s_object(label): 25 | k8s_object_list = ["service", "ingress", "configmap", "deployments", "pod", "job"] 26 | for k8s_object in k8s_object_list: 27 | command_to_delete_k8s_object = ('kubectl delete '+ k8s_object +' --selector branch=egapro-'+label) 28 | check_output(command_to_delete_k8s_object, shell=True) 29 | 30 | def get_k8s_tag_to_delete(active_k8s_tag_list, active_branch_list): 31 | k8s_tag_list_to_delete = [] 32 | if active_k8s_tag_list is not None: 33 | for active_k8s_tag in active_k8s_tag_list: 34 | delete_tag = False 35 | if active_k8s_tag != '': 36 | for active_branch in active_branch_list: 37 | if active_k8s_tag == active_branch : 38 | delete_tag = False 39 | break 40 | else: 41 | delete_tag = True 42 | if(delete_tag): 43 | k8s_tag_list_to_delete.append(active_k8s_tag) 44 | print(k8s_tag_list_to_delete) 45 | return k8s_tag_list_to_delete 46 | 47 | if __name__ == '__main__': 48 | for k8s_tag_to_delete in get_k8s_tag_to_delete(get_active_k8s_tags(), get_active_branches()): 49 | delete_k8s_object(k8s_tag_to_delete) 50 | print('k8s objects with label branch=egapro-'+k8s_tag_to_delete+' have been deleted') 51 | -------------------------------------------------------------------------------- /packages/app/src/components/ResultBubble.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import globalStyles from "../utils/globalStyles"; 5 | 6 | import Bubble from "./Bubble"; 7 | 8 | export interface Props { 9 | firstLineLabel: string; 10 | firstLineData: string; 11 | firstLineInfo?: string; 12 | secondLineLabel: string; 13 | secondLineData: string; 14 | secondLineInfo?: string; 15 | indicateurSexeSurRepresente?: "hommes" | "femmes" | undefined; 16 | } 17 | 18 | function ResultBubble({ 19 | firstLineLabel, 20 | firstLineData, 21 | firstLineInfo, 22 | secondLineLabel, 23 | secondLineData, 24 | secondLineInfo, 25 | indicateurSexeSurRepresente 26 | }: Props) { 27 | return ( 28 | 34 |
35 |

36 | {firstLineLabel} 37 | {firstLineData} 38 |

39 | {firstLineInfo &&

{firstLineInfo}

} 40 |
41 | 42 |
43 |

44 | {secondLineLabel} 45 | {secondLineData} 46 |

47 | {secondLineInfo &&

{secondLineInfo}

} 48 |
49 |
50 | ); 51 | } 52 | 53 | const styles = { 54 | blocWomen: css({ 55 | backgroundColor: globalStyles.colors.women, 56 | "@media print": { 57 | color: globalStyles.colors.women, 58 | border: `solid ${globalStyles.colors.women} 1px` 59 | } 60 | }), 61 | blocMen: css({ 62 | backgroundColor: globalStyles.colors.men, 63 | "@media print": { 64 | color: globalStyles.colors.men, 65 | border: `solid ${globalStyles.colors.men} 1px` 66 | } 67 | }), 68 | blocInfo: css({ 69 | borderBottom: "1px solid #FFFFFF" 70 | }), 71 | message: css({ 72 | marginBottom: 2, 73 | display: "flex", 74 | alignItems: "baseline", 75 | 76 | fontSize: 14, 77 | lineHeight: "17px" 78 | }), 79 | messageLabel: css({ 80 | marginRight: "auto" 81 | }), 82 | messageData: css({ 83 | fontWeight: "bold" 84 | }), 85 | info: css({ 86 | marginTop: 7, 87 | fontSize: 12, 88 | fontStyle: "italic", 89 | lineHeight: "15px" 90 | }) 91 | }; 92 | 93 | export default ResultBubble; 94 | -------------------------------------------------------------------------------- /packages/app/src/components/ModalContext.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { useRef, useContext, createContext, ReactNode } from "react"; 4 | import ReactDOM from "react-dom"; 5 | 6 | // Context 7 | 8 | const defaultValue: { container: null | Element } = { container: null }; 9 | 10 | export const ModalContext = createContext(defaultValue); 11 | 12 | // Provider 13 | 14 | function ModalProvider({ children }: { children: ReactNode }) { 15 | const containerRef = useRef(null); 16 | const container = containerRef.current; 17 | 18 | return ( 19 | 20 | {children} 21 |
22 | 23 | ); 24 | } 25 | 26 | export default ModalProvider; 27 | 28 | // Component 29 | 30 | export function Modal({ 31 | children, 32 | isOpen, 33 | onRequestClose 34 | }: { 35 | children: ReactNode; 36 | isOpen: boolean; 37 | onRequestClose: () => void; 38 | }) { 39 | const { container } = useContext(ModalContext); 40 | 41 | const child = ( 42 |
43 |
47 |
{children}
48 |
49 | ); 50 | 51 | return container ? ReactDOM.createPortal(child, container) : null; 52 | } 53 | 54 | const styles = { 55 | container: css({ 56 | position: "absolute", 57 | top: 0, 58 | left: 0, 59 | right: 0, 60 | bottom: 0, 61 | 62 | display: "flex", 63 | 64 | visibility: "hidden", 65 | transition: "visibility 0ms linear 250ms" 66 | }), 67 | containerOpen: css({ 68 | visibility: "visible", 69 | transitionDelay: "0ms" 70 | }), 71 | overlay: css({ 72 | position: "absolute", 73 | top: 0, 74 | left: 0, 75 | right: 0, 76 | bottom: 0, 77 | 78 | backgroundColor: "rgba(0,0,0,0.5)", 79 | backdropFilter: "blur(6px)", 80 | 81 | opacity: 0, 82 | transition: "opacity 175ms ease-in-out 75ms" 83 | }), 84 | overlayOpen: css({ 85 | opacity: 1, 86 | transitionDelay: "0ms" 87 | }), 88 | modal: css({ 89 | position: "relative", 90 | margin: "auto", 91 | 92 | transition: 93 | "opacity 250ms ease-in-out 0ms, transform 250ms ease-in-out 0ms", 94 | opacity: 0, 95 | transform: "scale(0.8)" 96 | }), 97 | modalOpen: css({ 98 | opacity: 1, 99 | transform: "initial" 100 | }) 101 | }; 102 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur1/IndicateurUnCoef/components/CoefGroupModalConfirmDelete.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import globalStyles from "../../../../utils/globalStyles"; 5 | 6 | import ActionLink from "../../../../components/ActionLink"; 7 | import ButtonAction from "../../../../components/ButtonAction"; 8 | 9 | import { IconWarning } from "../../../../components/Icons"; 10 | import { 11 | useColumnsWidth, 12 | useLayoutType 13 | } from "../../../../components/GridContext"; 14 | 15 | function ModalConfirmDelete({ 16 | closeModal, 17 | deleteGroup 18 | }: { 19 | closeModal: () => void; 20 | deleteGroup: () => void; 21 | }) { 22 | const layoutType = useLayoutType(); 23 | const width = useColumnsWidth(layoutType === "desktop" ? 6 : 7); 24 | 25 | return ( 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |

34 | Êtes vous sûr de vouloir supprimer ce groupe ? 35 |

36 |

37 | toutes les données renseignées pour ce groupes seront effacées 38 | définitivement. 39 |

40 | 41 |
42 | { 44 | deleteGroup(); 45 | closeModal(); 46 | }} 47 | label="supprimer" 48 | /> 49 |
50 | annuler 51 |
52 |
53 |
54 |
55 | ); 56 | } 57 | 58 | const styles = { 59 | modalConfirm: css({ 60 | width: 616, 61 | padding: "17px 16px 18px", 62 | backgroundColor: "#F6F7FF", 63 | borderRadius: 5 64 | }), 65 | 66 | bloc: css({ 67 | display: "flex" 68 | }), 69 | blocIcon: { 70 | marginRight: 22, 71 | color: globalStyles.colors.primary 72 | }, 73 | 74 | blocTitle: css({ 75 | fontSize: 18, 76 | lineHeight: "22px", 77 | textTransform: "uppercase", 78 | color: globalStyles.colors.primary 79 | }), 80 | blocText: css({ 81 | fontSize: 18, 82 | lineHeight: "22px", 83 | color: globalStyles.colors.primary 84 | }), 85 | 86 | actionBar: css({ 87 | display: "flex", 88 | flexDirection: "row", 89 | alignItems: "center", 90 | marginTop: 16 91 | }), 92 | 93 | spacerActionBarModal: css({ 94 | width: 21 95 | }) 96 | }; 97 | 98 | export default ModalConfirmDelete; 99 | -------------------------------------------------------------------------------- /packages/api/src/util/logger/logger.test.ts: -------------------------------------------------------------------------------- 1 | const now = 1570137016663; 2 | 3 | describe("test the output of the logger", () => { 4 | const sentry = require("@sentry/node"); 5 | let mockStdout: jest.SpyInstance; 6 | let logger: any; 7 | 8 | beforeEach(() => { 9 | // Mock stdout so we can check it's been called by the logger. 10 | mockStdout = jest.spyOn(global.process.stdout, "write"); 11 | // jest.spyOn will still call the underlying function. We don't want logs to 12 | // be displayed during the tests. 13 | mockStdout.mockImplementation(() => {}); 14 | // Mock `Date.now()` so we have a comparable timestamp. 15 | global.Date.now = jest.fn(() => now); 16 | // Mock `captureException` from Sentry to check that it's correctly called. 17 | sentry.captureException = jest.fn(); 18 | // Mock the `configuration` module to fake an `apiSentryDsn`. 19 | jest.mock("../../configuration", () => ({ 20 | configuration: { apiSentryDsn: "some sentry dsn" } 21 | })); 22 | // Only now require the logger module so it uses the mocks. 23 | logger = require("./").logger; 24 | }); 25 | 26 | afterEach(() => { 27 | jest.restoreAllMocks(); 28 | }); 29 | 30 | test("info uses console.log", () => { 31 | logger.info("just an info"); 32 | expect(mockStdout).toHaveBeenCalledTimes(1); 33 | // Grab the latest mock call, and get its first (and only) argument. 34 | const latestCall = mockStdout.mock.calls.pop(); 35 | const log = (latestCall && latestCall[0]) || ""; 36 | // Contains the timestamp. 37 | expect(log).toContain(now); 38 | // Contains the message. 39 | expect(log).toContain("just an info"); 40 | // Sentry.captureException shouldn't have been called. 41 | expect(sentry.captureException).not.toHaveBeenCalled(); 42 | }); 43 | test("error uses console.log and includes the stack trace", () => { 44 | const error = new Error("this is an error"); 45 | logger.error("just an error", error); 46 | expect(mockStdout).toHaveBeenCalledTimes(1); 47 | // Grab the latest mock call, and get its first (and only) argument. 48 | const latestCall = mockStdout.mock.calls.pop(); 49 | const log = (latestCall && latestCall[0]) || ""; 50 | // Contains the timestamp. 51 | expect(log).toContain(now); 52 | // Contains the message. 53 | expect(log).toContain("just an error"); 54 | // Contains the error. 55 | expect(log).toContain("this is an error"); 56 | // ... and contains the stack trace. 57 | expect(log).toContain(JSON.stringify(error.stack)); 58 | // Sentry.captureException should have been called with the error. 59 | expect(sentry.captureException).toHaveBeenCalledWith(error); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/app/src/__fixtures__/stateCompleteAndValidate.tsx: -------------------------------------------------------------------------------- 1 | import { FormState } from "../globals.d"; 2 | import AppReducer from "../AppReducer"; 3 | 4 | import stateComplete from "./stateComplete"; 5 | 6 | const actionValidateInformations = { 7 | type: "validateInformations" as "validateInformations", 8 | valid: "Valid" as FormState 9 | }; 10 | 11 | const actionValidateEffectif = { 12 | type: "validateEffectif" as "validateEffectif", 13 | valid: "Valid" as FormState 14 | }; 15 | 16 | const actionValidateIndicateurUnCoefGroup = { 17 | type: "validateIndicateurUnCoefGroup" as "validateIndicateurUnCoefGroup", 18 | valid: "Valid" as FormState 19 | }; 20 | 21 | const actionValidateIndicateurUnCoefEffectif = { 22 | type: "validateIndicateurUnCoefEffectif" as "validateIndicateurUnCoefEffectif", 23 | valid: "Valid" as FormState 24 | }; 25 | 26 | const actionValidateIndicateurUn = { 27 | type: "validateIndicateurUn" as "validateIndicateurUn", 28 | valid: "Valid" as FormState 29 | }; 30 | 31 | const actionValidateIndicateurDeux = { 32 | type: "validateIndicateurDeux" as "validateIndicateurDeux", 33 | valid: "Valid" as FormState 34 | }; 35 | 36 | const actionValidateIndicateurTrois = { 37 | type: "validateIndicateurTrois" as "validateIndicateurTrois", 38 | valid: "Valid" as FormState 39 | }; 40 | 41 | const actionValidateIndicateurDeuxTrois = { 42 | type: "validateIndicateurDeuxTrois" as "validateIndicateurDeuxTrois", 43 | valid: "Valid" as FormState 44 | }; 45 | 46 | const actionValidateIndicateurQuatre = { 47 | type: "validateIndicateurQuatre" as "validateIndicateurQuatre", 48 | valid: "Valid" as FormState 49 | }; 50 | 51 | const actionValidateIndicateurCinq = { 52 | type: "validateIndicateurCinq" as "validateIndicateurCinq", 53 | valid: "Valid" as FormState 54 | }; 55 | 56 | // fast pipe, I miss you in JS… 57 | const stateDefault = AppReducer( 58 | AppReducer( 59 | AppReducer( 60 | AppReducer( 61 | AppReducer( 62 | AppReducer( 63 | AppReducer( 64 | AppReducer( 65 | AppReducer( 66 | AppReducer(stateComplete, actionValidateInformations), 67 | actionValidateEffectif 68 | ), 69 | actionValidateIndicateurUnCoefGroup 70 | ), 71 | actionValidateIndicateurUnCoefEffectif 72 | ), 73 | actionValidateIndicateurUn 74 | ), 75 | actionValidateIndicateurDeux 76 | ), 77 | actionValidateIndicateurTrois 78 | ), 79 | actionValidateIndicateurDeuxTrois 80 | ), 81 | actionValidateIndicateurQuatre 82 | ), 83 | actionValidateIndicateurCinq 84 | ); 85 | 86 | export default stateDefault; 87 | -------------------------------------------------------------------------------- /packages/app/src/views/Effectif/EffectifResult.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | 4 | import { FormState } from "../../globals.d"; 5 | 6 | import globalStyles from "../../utils/globalStyles"; 7 | 8 | import Bubble from "../../components/Bubble"; 9 | import ActionLink from "../../components/ActionLink"; 10 | 11 | interface Props { 12 | totalNombreSalariesHomme: number; 13 | totalNombreSalariesFemme: number; 14 | validateEffectif: (valid: FormState) => void; 15 | } 16 | 17 | function EffectifResult({ 18 | totalNombreSalariesHomme, 19 | totalNombreSalariesFemme, 20 | validateEffectif 21 | }: Props) { 22 | return ( 23 |
24 | 25 |
26 |

27 | Nombre de femmes 28 | {totalNombreSalariesFemme} 29 |

30 |

31 | Nombre d’hommes 32 | {totalNombreSalariesHomme} 33 |

34 |
35 |

36 | Total effectifs 37 | 38 | {totalNombreSalariesFemme + totalNombreSalariesHomme} 39 | 40 |

41 |
42 | 43 |

44 | validateEffectif("None")}> 45 | modifier les données saisies 46 | 47 |

48 |
49 | ); 50 | } 51 | 52 | const styles = { 53 | container: css({ 54 | maxWidth: 250, 55 | marginTop: 64 56 | }), 57 | edit: css({ 58 | marginTop: 14, 59 | marginBottom: 14, 60 | textAlign: "center" 61 | }), 62 | 63 | bubble: css({ 64 | backgroundColor: "white", 65 | color: globalStyles.colors.default, 66 | border: "solid #EFECEF 1px", 67 | "@media print": { 68 | backgroundColor: "white", 69 | color: globalStyles.colors.default, 70 | border: "solid #EFECEF 1px" 71 | } 72 | }), 73 | 74 | blocNumbers: css({ 75 | height: 50, 76 | display: "flex", 77 | flexDirection: "column", 78 | justifyContent: "space-between" 79 | }), 80 | 81 | message: css({ 82 | marginBottom: 2, 83 | display: "flex", 84 | alignItems: "baseline", 85 | 86 | fontSize: 14, 87 | lineHeight: "17px" 88 | }), 89 | messageLabel: css({ 90 | marginRight: "auto" 91 | }), 92 | messageData: css({ 93 | fontWeight: "bold" 94 | }) 95 | }; 96 | 97 | export default EffectifResult; 98 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-detail-calcul/FAQIndicateur3DetailCalcul.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp, IconText } from "../../../components/Icons"; 6 | 7 | import FAQStep from "../components/FAQStep"; 8 | import FAQCalculScale from "../components/FAQCalculScale"; 9 | import FAQTitle3 from "../components/FAQTitle3"; 10 | 11 | function FAQIndicateur3DetailCalcul() { 12 | return ( 13 | 14 | Calculer l’indicateur 15 | 16 | }> 17 | Les groupes ne comportant pas{" "} 18 | au moins 10 hommes et 10 femmes ne sont pas retenus 19 | pour le calcul. 20 | 21 | 22 | 1}> 23 | Calculer le pourcentage de femmes et d’hommes promus au cours de la 24 | période de référence par catégories socio-professionnelles. 25 | 26 | 27 | 2}> 28 | Puis soustraire pour chaque groupe le pourcentage de femmes promues à 29 | celui des hommes promus. 30 | 31 | 32 | 3}> 33 | Pondérer les résultats obtenus en fonction de l’effectif du groupe par 34 | rapport à l’effectif total des groupes valides. 35 | 36 | 37 | 4}> 38 | Enfin additionner les résultats des différents groupes pour obtenir 39 | l’ecart global de taux de promotion entre les hommes et les femmes. 40 |
41 | * la valeur est exprimée en valeur absolue 42 |
43 | 44 |
45 | Appliquer le barème pour obtenir votre note 46 | 47 | 58 | 59 | }> 60 | Si l’écart constaté joue en faveur du sexe le moins bien rémunéré 61 | (indicateur 1), la note maximale de 15 points est attribuée à 62 | l’entreprise (considérant que l’employeur a mis en place une politique 63 | de rattrapage adaptée) 64 | 65 |
66 |
67 | ); 68 | } 69 | 70 | const styles = { 71 | content: css({ 72 | marginTop: 30 73 | }) 74 | }; 75 | 76 | export default FAQIndicateur3DetailCalcul; 77 | -------------------------------------------------------------------------------- /packages/app/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import globalStyles from "../utils/globalStyles"; 6 | 7 | import { useColumnsWidth, useLayoutType } from "./GridContext"; 8 | 9 | import Logo from "./Logo"; 10 | 11 | function Header() { 12 | const width = useColumnsWidth(2); 13 | const layoutType = useLayoutType(); 14 | return ( 15 |
16 |
23 | 32 | 33 | 34 |
35 |
36 | 37 | Index Egapro 38 | 39 |

40 | L’outil de calcul de votre index égalité professionnelle Femmes- 41 | Hommes 42 |

43 |
44 |
45 | ); 46 | } 47 | 48 | const styles = { 49 | header: css({ 50 | backgroundColor: "#FFF", 51 | height: 80, 52 | flexShrink: 0, 53 | display: "flex", 54 | flexDirection: "row", 55 | alignItems: "center", 56 | justifyContent: "center", 57 | borderBottom: "1px solid #EFECEF" 58 | }), 59 | headerLeft: css({ 60 | display: "flex", 61 | flexDirection: "row", 62 | marginLeft: globalStyles.grid.gutterWidth, 63 | marginRight: globalStyles.grid.gutterWidth, 64 | "@media print": { 65 | marginLeft: 0 66 | } 67 | }), 68 | headerLeftPrint: css({ 69 | "@media print": { 70 | width: "auto" 71 | } 72 | }), 73 | containerLogo: css({ 74 | marginLeft: "auto", 75 | marginRight: 0, 76 | textDecoration: "none", 77 | color: "currentColor" 78 | }), 79 | containerLogoDesktop: css({ 80 | marginRight: 25 81 | }), 82 | headerInner: css({ 83 | display: "flex", 84 | flexDirection: "row", 85 | flexGrow: 1, 86 | alignItems: "baseline" 87 | }), 88 | title: css({ 89 | fontFamily: "'Gabriela', serif", 90 | marginRight: 24, 91 | fontSize: 24, 92 | color: globalStyles.colors.default, 93 | textDecoration: "none" 94 | }), 95 | subtitle: css({ 96 | fontFamily: "'Gabriela', serif", 97 | fontSize: 12 98 | }) 99 | }; 100 | 101 | export default Header; 102 | -------------------------------------------------------------------------------- /packages/app/src/views/FAQ/components-detail-calcul/FAQIndicateur2DetailCalcul.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { Fragment } from "react"; 4 | 5 | import { IconLamp, IconText } from "../../../components/Icons"; 6 | 7 | import FAQStep from "../components/FAQStep"; 8 | import FAQCalculScale from "../components/FAQCalculScale"; 9 | import FAQTitle3 from "../components/FAQTitle3"; 10 | 11 | function FAQIndicateur2DetailCalcul() { 12 | return ( 13 | 14 | Calculer l’indicateur 15 | 16 | }> 17 | Les groupes ne comportant pas{" "} 18 | au moins 10 hommes et 10 femmes ne sont pas retenus 19 | pour le calcul. 20 | 21 | 22 | 1}> 23 | Calculer le pourcentage de femmes et d’hommes augmentés au cours de la 24 | période de référence par catégories socio-professionnelles. 25 | 26 | 27 | 2}> 28 | Puis soustraire pour chaque groupe le pourcentage de femmes augmentées à 29 | celui des hommes augmenté. 30 | 31 | 32 | 3}> 33 | Pondérer les résultats obtenus en fonction de l’effectif du groupe par 34 | rapport à l’effectif total des groupes valides. 35 | 36 | 37 | 4}> 38 | Enfin additionner les résultats des différents groupes pour obtenir 39 | l’ecart global de taux de d'augmentation entre les hommes et les femmes. 40 |
41 | * la valeur est exprimée en valeur absolue 42 |
43 | 44 |
45 | Appliquer le barème pour obtenir votre note 46 | 47 | 58 | 59 | }> 60 | Si l’écart constaté joue en faveur du sexe le moins bien rémunéré 61 | (indicateur 1), la note maximale de 20 points est attribuée à 62 | l’entreprise (considérant que l’employeur a mis en place une politique 63 | de rattrapage adaptée) 64 | 65 |
66 |
67 | ); 68 | } 69 | 70 | const styles = { 71 | content: css({ 72 | marginTop: 30 73 | }) 74 | }; 75 | 76 | export default FAQIndicateur2DetailCalcul; 77 | -------------------------------------------------------------------------------- /packages/app/src/views/Indicateur5/IndicateurCinq.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from "@emotion/core"; 3 | import { useCallback, ReactNode } from "react"; 4 | import { RouteComponentProps } from "react-router-dom"; 5 | 6 | import { 7 | AppState, 8 | FormState, 9 | ActionType, 10 | ActionIndicateurCinqData 11 | } from "../../globals.d"; 12 | 13 | import calculIndicateurCinq from "../../utils/calculsEgaProIndicateurCinq"; 14 | 15 | import Page from "../../components/Page"; 16 | import LayoutFormAndResult from "../../components/LayoutFormAndResult"; 17 | 18 | import IndicateurCinqForm from "./IndicateurCinqForm"; 19 | import IndicateurCinqResult from "./IndicateurCinqResult"; 20 | 21 | interface Props extends RouteComponentProps { 22 | state: AppState; 23 | dispatch: (action: ActionType) => void; 24 | } 25 | 26 | function IndicateurCinq({ state, dispatch }: Props) { 27 | const updateIndicateurCinq = useCallback( 28 | (data: ActionIndicateurCinqData) => 29 | dispatch({ type: "updateIndicateurCinq", data }), 30 | [dispatch] 31 | ); 32 | 33 | const validateIndicateurCinq = useCallback( 34 | (valid: FormState) => dispatch({ type: "validateIndicateurCinq", valid }), 35 | [dispatch] 36 | ); 37 | 38 | const { 39 | indicateurSexeSousRepresente, 40 | indicateurNombreSalariesSexeSousRepresente, 41 | noteIndicateurCinq 42 | } = calculIndicateurCinq(state); 43 | 44 | return ( 45 | 46 | 54 | } 55 | childrenResult={ 56 | state.indicateurCinq.formValidated === "Valid" && ( 57 | 65 | ) 66 | } 67 | /> 68 | 69 | ); 70 | } 71 | 72 | function PageIndicateurCinq({ children }: { children: ReactNode }) { 73 | return ( 74 | 78 | {children} 79 | 80 | ); 81 | } 82 | 83 | export default IndicateurCinq; 84 | -------------------------------------------------------------------------------- /packages/app/src/components/RadiosBoolean.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from "@emotion/core"; 3 | import { ReactNode } from "react"; 4 | import { useField } from "react-final-form"; 5 | 6 | import globalStyles from "../utils/globalStyles"; 7 | 8 | function RadioField({ 9 | fieldName, 10 | label, 11 | value, 12 | disabled 13 | }: { 14 | fieldName: string; 15 | label: ReactNode; 16 | value: string; 17 | disabled: boolean; 18 | }) { 19 | const { input } = useField(fieldName, { type: "radio", value }); 20 | return ( 21 |