node
190
node/frontend-react/src/App.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { LicenseManager } from 'ag-grid-enterprise';
|
||||
import axios from 'axios';
|
||||
import { observer } from 'mobx-react';
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { SWRConfig } from 'swr';
|
||||
import { Chargement } from './components/Commun/Chargement';
|
||||
import { Erreur } from './components/Commun/Erreur';
|
||||
import { Login } from './components/Commun/Login';
|
||||
import EntetePage from './components/EntetePage/EntetePage';
|
||||
import GestionDecisionBarreNavParamsActions from './components/EnteteVues/GestionDecisionBarreNavParamsActions';
|
||||
import GestionDecision from './components/GestionDecision/GestionDecision';
|
||||
import GestionITD from './components/GestionPerimetre/GestionITD';
|
||||
import GestionPerimetre from './components/GestionPerimetre/GestionPerimetre';
|
||||
// import Indicateurs from './components/Indicateurs/Indicateurs';
|
||||
import { NAVIGATION, PROFILS } from './constantes/constantes';
|
||||
import AppConfigStore, { AppConfigContext } from './stores/AppConfigStore';
|
||||
|
||||
//TODO faut-il stocker la license dans un fichier de propriétés modifiable en dehors du code compilé ?
|
||||
LicenseManager.setLicenseKey(
|
||||
'CompanyName=SMLB-NEXT_on_behalf_of_DIR DES RESS. HUMAINES DE LEMAA,LicensedApplication=OGURE NG,LicenseType=SingleApplication,LicensedConcurrentDeveloperCount=1,LicensedProductionInstancesCount=1,AssetReference=AG-019349,ExpiryDate=24_September_2022_[v2]_MTY2Mzk3NDAwMDAwMA==7903ffcc2e2649261b5331bada95402c'
|
||||
);
|
||||
|
||||
let appConfig = new AppConfigStore();
|
||||
|
||||
/**
|
||||
* Composant racine de l'application Ogure NG.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
const App = observer(() => {
|
||||
// const routeWithoutSousVivier = NAVIGATION.INDICATEURS
|
||||
const routeWithoutDroit = '/'
|
||||
const hasSousVivier = appConfig.sousViviersUtilisateurConnecte?.length;
|
||||
const isSuper = !appConfig.profilSelectionne && appConfig.hasProfil(PROFILS.SUPER);
|
||||
|
||||
return appConfig.ready ? (
|
||||
!appConfig.error ? (
|
||||
appConfig.utilisateurConnecte ? (
|
||||
<SWRConfig
|
||||
value={{
|
||||
refreshInterval: 10000,
|
||||
fetcher: (url) =>
|
||||
axios(url).then((res) => {
|
||||
// console.log('swr res', res);
|
||||
return res?.data;
|
||||
})
|
||||
}}
|
||||
>
|
||||
<AppConfigContext.Provider value={appConfig}>
|
||||
<Router basename={''}>
|
||||
<div className="App container-fluid p-0 d-flex flex-column align-items-stretch">
|
||||
<EntetePage />
|
||||
|
||||
<GestionDecisionBarreNavParamsActions />
|
||||
|
||||
<div className="corpsDePage d-flex flex-column align-items-stretch" role="main">
|
||||
<Switch>
|
||||
<Route exact path="/" render={
|
||||
() => <Redirect to={appConfig.profilSelectionne == PROFILS.ITD
|
||||
? '/GestionITD'
|
||||
: [PROFILS.PCP, PROFILS.FILIERE, PROFILS.BVT].includes(appConfig.profilSelectionne)
|
||||
? NAVIGATION.PERIMETRE
|
||||
: routeWithoutDroit} />
|
||||
} />
|
||||
<Route path={NAVIGATION.PERIMETRE} render={
|
||||
() => [PROFILS.PCP, PROFILS.FILIERE, PROFILS.BVT].includes(appConfig.profilSelectionne)
|
||||
? <GestionPerimetre />
|
||||
: <Redirect to={routeWithoutDroit} />
|
||||
} />
|
||||
<Route path="/GestionITD" render={
|
||||
() => [PROFILS.ITD, PROFILS.FILIERE, PROFILS.PCP].includes(appConfig.profilSelectionne)
|
||||
? <GestionITD />
|
||||
: <Redirect to={routeWithoutDroit} />
|
||||
} />
|
||||
<Route path={NAVIGATION.GESTION_DECISIONS} render={
|
||||
() => appConfig.profilSelectionne == PROFILS.FILIERE
|
||||
? <GestionDecision />
|
||||
: <Redirect to={routeWithoutDroit} />
|
||||
// () => !isSuper && !appConfig.profilSelectionne
|
||||
// ? <Redirect to={routeWithoutDroit} />
|
||||
// : hasSousVivier ? <GestionDecision /> : <Redirect to={routeWithoutSousVivier} />
|
||||
} />
|
||||
<Route path={NAVIGATION.INDICATEURS} render={
|
||||
() => <Redirect to={routeWithoutDroit} />
|
||||
} />
|
||||
{/* <Route path={routeWithoutSousVivier}>
|
||||
<Indicateurs />
|
||||
</Route> */}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</AppConfigContext.Provider>
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
/>
|
||||
</SWRConfig>
|
||||
) : (
|
||||
<Login />
|
||||
)
|
||||
) : (
|
||||
<Erreur />
|
||||
)
|
||||
) : (
|
||||
<div className={'vh-100'}>
|
||||
<Chargement />
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
/>{' '}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
export default App;
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
Base d'authentification + routage. Cf. https://reactrouter.com/web/example/auth-workflow
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// A wrapper for <Route> that redirects to the login
|
||||
// screen if you're not yet authenticated.
|
||||
function PrivateRoute({ children, ...rest }) {
|
||||
let auth = useAuth();
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ location }) =>
|
||||
auth.user ? (
|
||||
children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/connexion',
|
||||
state: { from: location }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ecran de connexion
|
||||
* @returns Elément REACT de l'écran de connexion
|
||||
*/
|
||||
function EcranConnexion() {
|
||||
let history = useHistory();
|
||||
let location = useLocation();
|
||||
let auth = useAuth();
|
||||
|
||||
let { from } = location.state || { from: { pathname: '/' } };
|
||||
|
||||
let login = (e) => {
|
||||
e.preventDefault();
|
||||
auth.signin(() => {
|
||||
history.replace(from);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<p>
|
||||
Vous devez{' '}
|
||||
<a href="#" onClick={login}>
|
||||
vous connecter
|
||||
</a>{' '}
|
||||
pour accéder à l'écran {from.pathname}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
node/frontend-react/src/App.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@import 'styles/dsfr.css';
|
||||
@import 'styles/custom-styles.scss';
|
||||
|
||||
.App {
|
||||
height: 100vh;
|
||||
|
||||
> .corpsDePage {
|
||||
flex-grow: 1;
|
||||
|
||||
> * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.InfoChargementApp {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 2em;
|
||||
|
||||
.spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
9
node/frontend-react/src/App.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
||||
201
node/frontend-react/src/assets/css/donnees-de-reference.css
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
----------------------------------------------------------------------
|
||||
MARQUES
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
/* .pictoMarque.ADM_BDG {
|
||||
background-color: rgb(100, 56, 56);
|
||||
} */
|
||||
/* .pictoMarque.ADM_FDC {
|
||||
background-color: cyan;
|
||||
}
|
||||
.pictoMarque.ADM_RDC {
|
||||
background-color: cyan;
|
||||
}
|
||||
.pictoMarque.GEN_COCHE {
|
||||
background-color: green;
|
||||
}
|
||||
.pictoMarque.GEN_DRAPEAU {
|
||||
background-color: orangered;
|
||||
}
|
||||
.pictoMarque.GEN_DRAPEAU2 {
|
||||
background-color: rgb(117, 60, 117);
|
||||
}
|
||||
.pictoMarque.GEN_ETOILE {
|
||||
background-color: rgb(52, 126, 52);
|
||||
}
|
||||
.pictoMarque.GEN_ETOILE2 {
|
||||
background-color: rgb(116, 69, 116);
|
||||
}
|
||||
|
||||
|
||||
.pictoMarque.PRIO_P1 {
|
||||
background-color: red;
|
||||
}
|
||||
.pictoMarque.PRIO_P2 {
|
||||
background-color: rgb(94, 50, 94);
|
||||
}
|
||||
.pictoMarque.PRIO_P3 {
|
||||
background-color: cyan;
|
||||
}
|
||||
.pictoMarque.PRIO_P4 {
|
||||
background-color: rgb(94, 173, 94);
|
||||
} */
|
||||
.pictoMarque.GEN_QUESTION {
|
||||
background-color: cyan;
|
||||
}
|
||||
.pictoMarque.GEN_QUESTION2 {
|
||||
background-color: rgb(93, 214, 93);
|
||||
}
|
||||
/* .pictoMarque.GEN_ALERTE {
|
||||
background-color: red;
|
||||
} */
|
||||
/*
|
||||
----------------------------------------------------------------------
|
||||
AVIS SUR LES ADMINISTRES
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
.avis.administre,
|
||||
.avis.administre._VIDE_ {
|
||||
text-align: left;
|
||||
}
|
||||
.avis.administre.A_ETUDIER {
|
||||
background-color: #d9e1f2 !important;
|
||||
}
|
||||
.avis.administre.A_MUTER {
|
||||
background-color: #e2efda !important;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
.avis.administre.A_MAINTENIR {
|
||||
background-color: #efd5fd !important;
|
||||
}
|
||||
.avis.administre.NON_ETUDIE {
|
||||
background-color: #fdd5d5 !important;
|
||||
}
|
||||
.avis.administre.NON_DISPONIBLE {
|
||||
background-color: #fdecd5 !important;
|
||||
}
|
||||
.avis.administre.PARTANT {
|
||||
background-color: #d8d8d8 !important;
|
||||
}
|
||||
|
||||
.ag-menu-option-active.avis.administre.A_ETUDIER,
|
||||
.ag-row-hover .avis.administre.A_ETUDIER {
|
||||
background-color: #d0d7e6 !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.administre.A_MUTER,
|
||||
.ag-row-hover .avis.administre.A_MUTER {
|
||||
background-color: #d4e0cc !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.administre.A_MAINTENIR,
|
||||
.ag-row-hover .avis.administre.A_MAINTENIR {
|
||||
background-color: #dbc2e9 !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.administre.NON_ETUDIE,
|
||||
.ag-row-hover .avis.administre.NON_ETUDIE {
|
||||
background-color: #e9c6c6 !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.administre.NON_DISPONIBLE,
|
||||
.ag-row-hover .avis.administre.NON_DISPONIBLE {
|
||||
background-color: #e9d9c3 !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.administre.PARTANT,
|
||||
.ag-row-hover .avis.administre.PARTANT {
|
||||
background-color: #c5c5c5 !important;
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------------------------------------------------
|
||||
AVIS SUR LES POSTES
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
.avis.poste,
|
||||
.avis.poste._VIDE_ {
|
||||
text-align: left;
|
||||
}
|
||||
.avis.poste.P1 {
|
||||
background-color: #e2efda !important;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
.avis.poste.P2 {
|
||||
background-color: #d9e1f2 !important;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
.avis.poste.P3 {
|
||||
background-color: #fff2cc !important;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
.avis.poste.P4 {
|
||||
background-color: #f6f6f6 !important;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
.avis.poste.NON_ETUDIE {
|
||||
background-color: #fdd5d5 !important;
|
||||
}
|
||||
.avis.poste.GELE {
|
||||
background-color: #d5fafd !important;
|
||||
}
|
||||
|
||||
.ag-menu-option-active.avis.poste.P1,
|
||||
.ag-row-hover .avis.poste.P1 {
|
||||
background-color: #d5e0ce !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.poste.P2,
|
||||
.ag-row-hover .avis.poste.P2 {
|
||||
background-color: #cad1e0 !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.poste.P3,
|
||||
.ag-row-hover .avis.poste.P3 {
|
||||
background-color: #ece1be !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.poste.P4,
|
||||
.ag-row-hover .avis.poste.P4 {
|
||||
background-color: #e7e7e7 !important;
|
||||
}
|
||||
.ag-menu-option-active.avis.poste.NON_ETUDIE,
|
||||
.ag-row-hover .avis.poste.NON_ETUDIE {
|
||||
background-color: #e7c3c3 !important;
|
||||
}
|
||||
.ag-menu-option-active.poste.administre.GELE,
|
||||
.ag-row-hover .avis.poste.GELE {
|
||||
background-color: #c7e9ec !important;
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------------------------------------------------
|
||||
DECISIONS SUR LES ADMINISTRES/POSTES
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
.decision,
|
||||
.decision._VIDE_ {
|
||||
text-align: left;
|
||||
}
|
||||
.decision.PREPOSITIONNE {
|
||||
background-color: #d9f2f1 !important;
|
||||
}
|
||||
.decision.POSITIONNE {
|
||||
background-color: #b9e8b7 !important;
|
||||
}
|
||||
.decision.OMI_EN_COURS {
|
||||
background-color: #f5e6be !important;
|
||||
}
|
||||
.decision.OMI_ACTIVE {
|
||||
background-color: #f6cece !important;
|
||||
}
|
||||
|
||||
.ag-row-hover .decision.PREPOSITIONNE,
|
||||
.ag-menu-option-active.decision.PREPOSITIONNE {
|
||||
background-color: #c8e0e0 !important;
|
||||
}
|
||||
.ag-row-hover .decision.POSITIONNE,
|
||||
.ag-menu-option-active.decision.POSITIONNE {
|
||||
background-color: #a9d6a8 !important;
|
||||
}
|
||||
.ag-row-hover .decision.OMI_EN_COURS,
|
||||
.ag-menu-option-active.decision.OMI_EN_COURS {
|
||||
background-color: #e6d8b3 !important;
|
||||
}
|
||||
.ag-row-hover .decision.OMI_ACTIVE,
|
||||
.ag-menu-option-active.decision.OMI_ACTIVE {
|
||||
background-color: #ebc4c4 !important;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="549" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -549)"><rect x="159" y="549" width="19" height="19" fill="#63E0E0"/><path d="M167.303 553.852 164.738 549.577C164.523 549.219 164.137 549 163.72 549L159.595 549C159.114 549 158.833 549.541 159.108 549.934L163.238 555.833C164.34 554.803 165.743 554.095 167.303 553.852ZM177.405 549 173.28 549C172.863 549 172.477 549.219 172.262 549.577L169.697 553.852C171.257 554.095 172.66 554.803 173.762 555.833L177.892 549.934C178.167 549.541 177.886 549 177.405 549ZM168.5 554.938C164.893 554.938 161.969 557.862 161.969 561.469 161.969 565.076 164.893 568 168.5 568 172.107 568 175.031 565.076 175.031 561.469 175.031 557.862 172.107 554.938 168.5 554.938ZM171.933 560.773 170.526 562.145 170.859 564.083C170.918 564.43 170.552 564.695 170.241 564.531L168.5 563.617 166.76 564.531C166.448 564.697 166.082 564.43 166.142 564.083L166.475 562.145 165.067 560.773C164.814 560.527 164.954 560.097 165.303 560.046L167.248 559.763 168.118 557.999C168.196 557.841 168.347 557.762 168.499 557.762 168.652 557.762 168.804 557.842 168.883 557.999L169.752 559.763 171.698 560.046C172.047 560.097 172.186 560.527 171.933 560.773Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 600 600" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="348" y="68" width="584" height="584"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-348 -68)"><rect x="348" y="68" width="584" height="584" fill="#996633"/><path d="M640 116.667C774.32 116.667 883.333 225.68 883.333 360 883.333 494.32 774.32 603.333 640 603.333 505.68 603.333 396.667 494.32 396.667 360 396.667 225.68 505.68 116.667 640 116.667ZM494.56 443.123C530.281 496.413 583.912 530.333 643.893 530.333 703.851 530.333 757.506 496.437 793.203 443.123 752.705 405.275 699.322 384.255 643.893 384.333 588.455 384.251 535.063 405.27 494.56 443.123ZM640 335.667C680.318 335.667 713 302.983 713 262.667 713 222.35 680.318 189.667 640 189.667 599.682 189.667 567 222.35 567 262.667 567 302.983 599.682 335.667 640 335.667Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1001 B |
33
node/frontend-react/src/assets/images/marques/ADM_FDC.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="61.000000pt" height="60.000000pt" viewBox="0 0 61.000000 60.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,60.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M22 458 c2 -6 23 -18 46 -27 49 -18 50 -24 18 -106 -19 -50 -27 -59
|
||||
-45 -57 -11 2 -21 -2 -21 -7 0 -6 3 -11 8 -11 4 0 17 -3 30 -6 16 -5 22 -2 22
|
||||
10 0 9 4 16 9 16 5 0 7 -8 4 -19 -2 -10 1 -25 9 -32 7 -8 18 -23 24 -34 6 -10
|
||||
19 -23 29 -29 11 -6 25 -17 33 -23 7 -7 19 -13 27 -13 7 0 15 -6 18 -14 7 -19
|
||||
92 -19 107 -1 6 7 19 11 30 8 12 -3 23 2 31 15 7 11 18 18 25 15 6 -2 18 5 26
|
||||
15 7 11 25 22 38 24 21 2 25 9 26 35 0 29 14 62 14 35 0 -17 23 -15 46 4 18
|
||||
15 18 15 -7 10 -24 -5 -27 -1 -50 62 -13 37 -24 71 -24 76 0 4 20 16 45 26 24
|
||||
9 46 22 48 29 5 14 -96 -24 -119 -45 -13 -12 -25 -12 -85 1 l-69 14 -51 -31
|
||||
c-39 -23 -54 -39 -62 -66 -9 -28 -8 -38 1 -44 21 -13 46 -9 66 11 10 10 29 22
|
||||
40 26 12 3 19 10 16 14 -8 14 -55 0 -65 -19 -7 -14 -50 -36 -50 -26 0 2 5 17
|
||||
11 35 7 20 26 40 53 56 40 24 43 24 109 10 37 -8 74 -12 81 -9 18 7 48 -79 40
|
||||
-112 l-6 -24 -76 40 c-83 44 -82 44 -82 31 0 -4 34 -26 76 -47 72 -37 95 -57
|
||||
80 -71 -3 -4 -22 1 -41 10 -51 24 -65 20 -30 -8 22 -18 27 -27 18 -36 -8 -8
|
||||
-19 -6 -43 6 -33 17 -59 11 -27 -6 9 -6 17 -17 17 -26 0 -15 -12 -15 -54 2
|
||||
-15 7 -17 5 -11 -13 5 -16 2 -22 -9 -22 -10 0 -16 9 -16 25 0 14 -7 28 -15 32
|
||||
-14 5 -45 32 -116 98 -8 8 -29 15 -47 17 -18 1 -32 2 -32 3 0 0 9 23 19 50 19
|
||||
49 20 50 56 47 25 -3 35 0 31 7 -4 6 -18 11 -32 11 -13 0 -24 5 -24 10 0 6
|
||||
-25 21 -56 35 -65 29 -67 29 -62 13z m128 -213 c0 -19 -20 -29 -32 -17 -8 8
|
||||
-7 16 2 26 14 17 30 12 30 -9z m46 -26 c10 -17 -28 -50 -46 -39 -12 7 -11 12
|
||||
4 29 21 24 32 26 42 10z m40 -40 c10 -17 -23 -46 -38 -31 -8 8 -8 15 2 27 15
|
||||
18 26 19 36 4z m44 -43 c0 -8 -5 -18 -11 -22 -14 -8 -33 11 -25 25 10 16 36
|
||||
13 36 -3z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="160" y="615" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-160 -615)"><rect x="160" y="615" width="19" height="19" fill="#63E0E0"/><path d="M165.34 623.89C167.334 623.89 168.957 622.268 168.957 620.273 168.957 618.279 167.334 616.656 165.34 616.656 163.345 616.656 161.723 618.279 161.723 620.273 161.723 622.268 163.345 623.89 165.34 623.89Z"/><path d="M173.66 623.89C175.655 623.89 177.277 622.268 177.277 620.273 177.277 618.279 175.655 616.656 173.66 616.656 171.666 616.656 170.043 618.279 170.043 620.273 170.043 622.268 171.666 623.89 173.66 623.89Z"/><path d="M165.34 624.523C162.395 624.523 160 626.919 160 629.863L160 631.674C160 632.044 160.3 632.344 160.67 632.344L170.009 632.344C170.379 632.344 170.679 632.044 170.679 631.674L170.679 629.863C170.679 626.949 168.331 624.523 165.34 624.523Z"/><path d="M170.288 625.718C170.194 625.795 170.181 625.934 170.256 626.029 171.784 627.979 171.573 629.864 171.573 631.674 171.573 631.809 171.555 631.941 171.523 632.066 171.486 632.207 171.593 632.344 171.739 632.344L178.33 632.344C178.7 632.344 179 632.044 179 631.674L179 629.863C179 625.323 173.704 622.92 170.288 625.718Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 27 27" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="155" y="510" width="27" height="27"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-155 -510)"><rect x="155" y="510" width="27" height="27" fill="#FF6600"/><path d="M173 524.625 173 522.375 162.875 522.375 162.875 519 157.25 523.5 162.875 528 162.875 524.625Z"/><path d="M177.5 513.375 167.375 513.375C166.134 513.375 165.125 514.384 165.125 515.625L165.125 520.125 167.375 520.125 167.375 515.625 177.5 515.625 177.5 531.375 167.375 531.375 167.375 526.875 165.125 526.875 165.125 531.375C165.125 532.616 166.134 533.625 167.375 533.625L177.5 533.625C178.741 533.625 179.75 532.616 179.75 531.375L179.75 515.625C179.75 514.384 178.741 513.375 177.5 513.375Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 932 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 23 22" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="356" y="469" width="23" height="22"/></clipPath><clipPath id="clip1"><rect x="-9653.67" y="-0.498398" width="241352" height="222043"/></clipPath><image width="32" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGYUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOf6h+wAAACHdFJOUwABAgMEBQYHCAkKDQ4PExQVFhcbHR4fJCUmKiwtLi82Nzg6Ozw+Q0VGR0hLTE9RUlNWWFpbXF1hYmNlZmdoaWptbm9wd3t8f4CFhomKi4yNj5SVlpebnJ6goaWmp6mqr7Gys7S1u7y+v8HLzM3Oz9TV1tfd3ufo6e3u7/Dx8vP09ff4+fv8/k/P8HIAAAAJcEhZcwAADpEAAA6RAQ6kPk4AAAGoSURBVDhPlZPnVxNREEcfoYOKYlRURETsDbuogBQFCyrSNfauKDbQKCW6S+6/7bx5kxAgx3O8H3bu/N7ZfTtb3P+xsXN4pGuTNUW4tICw2GHtGi7LahzLodOCVSQzzJ+trDgzR2arRStJwUlf2+ChBqtozfI62EvYF6yQxHuWdgXdGfOhNGgB7TBs6obgommedbOk68zdhh98X2+e4xb0SKm9PVgr5Qrc0ThPQ8Tncqk34YaUsmmi7bqQ4ykc8vUBjPt6AJ77muMgPFO5B3dVnsBhFaXsE1GD2nXoV9kW8cXvGeiGwWB90BtM7tpMp5q1qTrgfDCZ+2dubtm43fQ0nDC9ACPBGmOmEkHdEdhv6p99k9orsq0qwl7YY5p/e8chpYGnMnW/wlTff5tEX8lssUQoX57Nf0HfqvyIAxYILel0s6nQ70d9x0yN9cIYDJkKNTNMud9MWOs5thQfNfVM8sctFtyikEyaKI+Yd2/5VW/tGjYv8Madg4+7SyxZQaJlGk65khfyp0SZIkSy8FhOrZ7MihUlO1ql12q8NjZRhNGrO3T5nzj3Fwr3jxV48vx4AAAAAElFTkSuQmCC" preserveAspectRatio="none" id="img2"></image><clipPath id="clip3"><rect x="0" y="0" width="212389" height="212389"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-356 -469)"><rect x="356" y="469" width="23.0001" height="22" fill="#FF0000"/><g clip-path="url(#clip1)" transform="matrix(0.000103583 0 0 0.000103583 356 469)"><g clip-path="url(#clip3)" transform="matrix(1.04546 0 0 1 -0.294619 -0.0982628)"><use width="100%" height="100%" xlink:href="#img2" transform="scale(6637.16 6637.16)"></use></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z"></path></svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16" rx="2"></rect></svg>
|
||||
|
After Width: | Height: | Size: 193 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 137 136" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="496" y="338" width="137" height="136"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-496 -338)"><rect x="496" y="338" width="137" height="136" fill="#92D050"/><path d="M133.875 68C133.875 104.382 104.382 133.875 68 133.875 31.6181 133.875 2.125 104.382 2.125 68 2.125 31.6181 31.6181 2.125 68 2.125 104.382 2.125 133.875 31.6181 133.875 68ZM60.3803 102.88 109.255 54.0053C110.915 52.3457 110.915 49.6546 109.255 47.995L103.245 41.9847C101.585 40.3248 98.8943 40.3248 97.2344 41.9847L57.375 81.8438 38.7656 63.2344C37.1059 61.5748 34.4149 61.5748 32.755 63.2344L26.7447 69.2447C25.0851 70.9043 25.0851 73.5954 26.7447 75.255L54.3697 102.88C56.0296 104.54 58.7204 104.54 60.3803 102.88Z" transform="matrix(1.00735 0 0 1 496 338)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1004 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="160" y="267" width="19" height="18"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-160 -267)"><rect x="160" y="267" width="19" height="18" fill="#FF6600"/><path d="M3.9375 1.125C4.24816 1.125 4.5 1.37684 4.5 1.6875L4.5 16.3125C4.5 16.6231 4.24816 16.875 3.9375 16.875 3.62684 16.875 3.375 16.6231 3.375 16.3125L3.375 1.6875C3.375 1.37684 3.62684 1.125 3.9375 1.125Z" fill-rule="evenodd" transform="matrix(1.05556 0 0 1 160 267)"/><path d="M4.23225 2.87775C5.32688 2.14762 6.0165 1.6875 7.3125 1.6875 8.04712 1.6875 8.59387 2.05313 8.99438 2.31975L9.03038 2.3445C9.47025 2.637 9.75713 2.8125 10.125 2.8125 10.314 2.8125 10.5255 2.76525 10.7854 2.67075 11.0111 2.58508 11.2336 2.49127 11.4525 2.3895 11.5177 2.35912 11.5841 2.32987 11.655 2.2995 12.2963 2.01262 13.0927 1.6875 14.0625 1.6875 14.3731 1.6875 14.625 1.93934 14.625 2.25L14.625 9C14.625 9.31066 14.3731 9.5625 14.0625 9.5625 13.3448 9.5625 12.735 9.79875 12.1117 10.0766L11.9329 10.1554C11.6854 10.2679 11.4233 10.386 11.1701 10.4783 10.8367 10.6086 10.4829 10.6795 10.125 10.6875 9.39038 10.6875 8.84363 10.3219 8.44312 10.0553L8.40713 10.0305C7.96725 9.738 7.68038 9.5625 7.3125 9.5625 6.37537 9.5625 5.94225 9.83925 4.81163 10.593 4.553 10.7651 4.20381 10.695 4.03171 10.4363 3.97033 10.3441 3.93756 10.2358 3.9375 10.125L3.9375 3.375C3.9376 3.18687 4.03174 3.01123 4.18837 2.907L4.23338 2.87775Z" fill-rule="evenodd" transform="matrix(1.05556 0 0 1 160 267)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="305" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -305)"><rect x="159" y="305" width="19" height="19" fill="#946CC4"/><path d="M163.156 306.188C163.484 306.188 163.75 306.453 163.75 306.781L163.75 322.219C163.75 322.547 163.484 322.812 163.156 322.812 162.828 322.812 162.562 322.547 162.562 322.219L162.562 306.781C162.562 306.453 162.828 306.188 163.156 306.188Z" fill-rule="evenodd"/><path d="M163.467 308.038C164.623 307.267 165.351 306.781 166.719 306.781 167.494 306.781 168.071 307.167 168.494 307.449L168.532 307.475C168.996 307.784 169.299 307.969 169.688 307.969 169.887 307.969 170.11 307.919 170.385 307.819 170.623 307.729 170.858 307.63 171.089 307.522 171.158 307.49 171.228 307.459 171.303 307.427 171.979 307.124 172.82 306.781 173.844 306.781 174.172 306.781 174.438 307.047 174.438 307.375L174.438 314.5C174.438 314.828 174.172 315.094 173.844 315.094 173.086 315.094 172.443 315.343 171.785 315.636L171.596 315.72C171.335 315.838 171.058 315.963 170.791 316.06 170.439 316.198 170.065 316.273 169.688 316.281 168.912 316.281 168.335 315.895 167.912 315.614L167.874 315.588C167.41 315.279 167.107 315.094 166.719 315.094 165.73 315.094 165.272 315.386 164.079 316.181 163.806 316.363 163.437 316.289 163.256 316.016 163.191 315.919 163.156 315.804 163.156 315.688L163.156 308.562C163.156 308.364 163.256 308.179 163.421 308.069L163.469 308.038Z" fill-rule="evenodd"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="156" y="378" width="19" height="18"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-156 -378)"><rect x="156" y="378" width="19" height="18" fill="#00B050"/><path d="M15.9627 6.20684 11.4996 5.5582 9.50449 1.51348C9.45 1.40273 9.36035 1.31309 9.24961 1.25859 8.97188 1.12148 8.63438 1.23574 8.49551 1.51348L6.50039 5.5582 2.0373 6.20684C1.91426 6.22441 1.80176 6.28242 1.71562 6.37031 1.4988 6.59317 1.50352 6.94956 1.72617 7.1666L4.95527 10.3148 4.19238 14.7604C4.13951 15.066 4.34442 15.3566 4.65005 15.4095 4.77236 15.4307 4.89823 15.4107 5.00801 15.3527L9 13.2539 12.992 15.3527C13.101 15.4107 13.2275 15.4301 13.3488 15.409 13.6547 15.3562 13.8604 15.0662 13.8076 14.7604L13.0447 10.3148 16.2738 7.1666C16.3617 7.08047 16.4197 6.96797 16.4373 6.84492 16.4848 6.5373 16.2703 6.25254 15.9627 6.20684Z" transform="matrix(1.05556 0 0 1 156 378)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="338" width="18" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -338)"><rect x="159" y="338" width="18" height="19" fill="#946CC4"/><path d="M15.9627 6.20684 11.4996 5.5582 9.50449 1.51348C9.45 1.40273 9.36035 1.31309 9.24961 1.25859 8.97188 1.12148 8.63438 1.23574 8.49551 1.51348L6.50039 5.5582 2.0373 6.20684C1.91426 6.22441 1.80176 6.28242 1.71562 6.37031 1.4988 6.59317 1.50352 6.94956 1.72617 7.1666L4.95527 10.3148 4.19238 14.7604C4.13951 15.066 4.34442 15.3566 4.65005 15.4095 4.77236 15.4307 4.89823 15.4107 5.00801 15.3527L9 13.2539 12.992 15.3527C13.101 15.4107 13.2275 15.4301 13.3488 15.409 13.6547 15.3562 13.8604 15.0662 13.8076 14.7604L13.0447 10.3148 16.2738 7.1666C16.3617 7.08047 16.4197 6.96797 16.4373 6.84492 16.4848 6.5373 16.2703 6.25254 15.9627 6.20684Z" transform="matrix(1 0 0 1.05556 159 338)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z"></path></svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="272.000000pt" height="271.000000pt" viewBox="0 0 272.000000 271.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,271.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1145 2694 c-190 -32 -387 -110 -554 -221 -48 -32 -132 -104 -191
|
||||
-163 -195 -194 -317 -418 -377 -692 -25 -117 -25 -409 0 -526 60 -274 182
|
||||
-498 377 -692 464 -465 1173 -532 1729 -163 48 32 132 104 191 163 196 195
|
||||
317 417 377 692 25 117 25 409 0 526 -60 274 -182 498 -377 692 -207 207 -463
|
||||
338 -755 385 -110 18 -312 18 -420 -1z m418 -139 c592 -103 1017 -604 1017
|
||||
-1200 0 -353 -147 -679 -414 -914 -457 -402 -1155 -402 -1612 0 -506 446 -556
|
||||
1212 -113 1715 184 209 432 348 708 398 103 19 309 19 414 1z"/>
|
||||
<path d="M1235 2120 c-218 -47 -385 -208 -385 -372 0 -46 5 -62 26 -87 45 -54
|
||||
143 -63 199 -19 14 11 42 52 61 92 57 113 91 141 186 152 139 16 239 -70 225
|
||||
-193 -7 -62 -47 -115 -142 -191 -155 -124 -199 -176 -214 -256 -11 -62 3 -117
|
||||
41 -153 26 -25 37 -28 93 -28 78 0 93 12 145 117 33 68 42 78 140 151 136 101
|
||||
181 147 222 221 31 57 33 67 33 161 0 89 -3 107 -29 163 -50 107 -158 189
|
||||
-301 227 -71 19 -243 28 -300 15z"/>
|
||||
<path d="M1291 920 c-75 -17 -131 -86 -131 -162 0 -65 24 -114 71 -145 76 -50
|
||||
162 -40 223 27 112 124 0 316 -163 280z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="272.000000pt" height="271.000000pt" viewBox="0 0 272.000000 271.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,271.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1145 2694 c-190 -32 -387 -110 -554 -221 -48 -32 -132 -104 -191
|
||||
-163 -195 -194 -317 -418 -377 -692 -25 -117 -25 -409 0 -526 60 -274 182
|
||||
-498 377 -692 464 -465 1173 -532 1729 -163 48 32 132 104 191 163 196 195
|
||||
317 417 377 692 25 117 25 409 0 526 -60 274 -182 498 -377 692 -207 207 -463
|
||||
338 -755 385 -110 18 -312 18 -420 -1z m418 -139 c592 -103 1017 -604 1017
|
||||
-1200 0 -353 -147 -679 -414 -914 -457 -402 -1155 -402 -1612 0 -506 446 -556
|
||||
1212 -113 1715 184 209 432 348 708 398 103 19 309 19 414 1z"/>
|
||||
<path d="M1235 2120 c-218 -47 -385 -208 -385 -372 0 -46 5 -62 26 -87 45 -54
|
||||
143 -63 199 -19 14 11 42 52 61 92 57 113 91 141 186 152 139 16 239 -70 225
|
||||
-193 -7 -62 -47 -115 -142 -191 -155 -124 -199 -176 -214 -256 -11 -62 3 -117
|
||||
41 -153 26 -25 37 -28 93 -28 78 0 93 12 145 117 33 68 42 78 140 151 136 101
|
||||
181 147 222 221 31 57 33 67 33 161 0 89 -3 107 -29 163 -50 107 -158 189
|
||||
-301 227 -71 19 -243 28 -300 15z"/>
|
||||
<path d="M1291 920 c-75 -17 -131 -86 -131 -162 0 -65 24 -114 71 -145 76 -50
|
||||
162 -40 223 27 112 124 0 316 -163 280z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"></circle></svg>
|
||||
|
After Width: | Height: | Size: 187 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.022 1.566a1.13 1.13 0 011.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 331 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 18" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="155" y="234" width="19" height="18"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-155 -234)"><rect x="155" y="234" width="19" height="18" fill="#FF0000"/><path d="M3.9375 1.125C4.24816 1.125 4.5 1.37684 4.5 1.6875L4.5 16.3125C4.5 16.6231 4.24816 16.875 3.9375 16.875 3.62684 16.875 3.375 16.6231 3.375 16.3125L3.375 1.6875C3.375 1.37684 3.62684 1.125 3.9375 1.125Z" fill-rule="evenodd" transform="matrix(1.05556 0 0 1 155 234)"/><path d="M4.23225 2.87775C5.32688 2.14762 6.0165 1.6875 7.3125 1.6875 8.04712 1.6875 8.59387 2.05313 8.99438 2.31975L9.03038 2.3445C9.47025 2.637 9.75713 2.8125 10.125 2.8125 10.314 2.8125 10.5255 2.76525 10.7854 2.67075 11.0111 2.58508 11.2336 2.49127 11.4525 2.3895 11.5177 2.35912 11.5841 2.32987 11.655 2.2995 12.2963 2.01262 13.0927 1.6875 14.0625 1.6875 14.3731 1.6875 14.625 1.93934 14.625 2.25L14.625 9C14.625 9.31066 14.3731 9.5625 14.0625 9.5625 13.3448 9.5625 12.735 9.79875 12.1117 10.0766L11.9329 10.1554C11.6854 10.2679 11.4233 10.386 11.1701 10.4783 10.8367 10.6086 10.4829 10.6795 10.125 10.6875 9.39038 10.6875 8.84363 10.3219 8.44312 10.0553L8.40713 10.0305C7.96725 9.738 7.68038 9.5625 7.3125 9.5625 6.37537 9.5625 5.94225 9.83925 4.81163 10.593 4.553 10.7651 4.20381 10.695 4.03171 10.4363 3.97033 10.3441 3.93756 10.2358 3.9375 10.125L3.9375 3.375C3.9376 3.18687 4.03174 3.01123 4.18837 2.907L4.23338 2.87775Z" fill-rule="evenodd" transform="matrix(1.05556 0 0 1 155 234)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="155" y="315" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-155 -315)"><rect x="155" y="315" width="19" height="19" fill="#B79DD8"/><path d="M159.156 316.188C159.484 316.188 159.75 316.453 159.75 316.781L159.75 332.219C159.75 332.547 159.484 332.812 159.156 332.812 158.828 332.812 158.562 332.547 158.562 332.219L158.562 316.781C158.562 316.453 158.828 316.188 159.156 316.188Z" fill-rule="evenodd"/><path d="M159.467 318.038C160.623 317.267 161.351 316.781 162.719 316.781 163.494 316.781 164.071 317.167 164.494 317.449L164.532 317.475C164.996 317.784 165.299 317.969 165.688 317.969 165.887 317.969 166.11 317.919 166.385 317.819 166.623 317.729 166.858 317.63 167.089 317.522 167.158 317.49 167.228 317.459 167.303 317.427 167.979 317.124 168.82 316.781 169.844 316.781 170.172 316.781 170.438 317.047 170.438 317.375L170.438 324.5C170.438 324.828 170.172 325.094 169.844 325.094 169.086 325.094 168.443 325.343 167.785 325.636L167.596 325.72C167.335 325.838 167.058 325.963 166.791 326.06 166.439 326.198 166.065 326.273 165.688 326.281 164.912 326.281 164.335 325.895 163.912 325.614L163.874 325.588C163.41 325.279 163.107 325.094 162.719 325.094 161.73 325.094 161.272 325.386 160.079 326.181 159.806 326.363 159.437 326.289 159.256 326.016 159.191 325.919 159.156 325.804 159.156 325.688L159.156 318.562C159.156 318.364 159.256 318.179 159.421 318.069L159.469 318.038Z" fill-rule="evenodd"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="155" y="396" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-155 -396)"><rect x="155" y="396" width="19" height="19" fill="#97EAEA"/><path d="M159.156 397.188C159.484 397.188 159.75 397.453 159.75 397.781L159.75 413.219C159.75 413.547 159.484 413.812 159.156 413.812 158.828 413.812 158.562 413.547 158.562 413.219L158.562 397.781C158.562 397.453 158.828 397.188 159.156 397.188Z" fill-rule="evenodd"/><path d="M159.467 399.038C160.623 398.267 161.351 397.781 162.719 397.781 163.494 397.781 164.071 398.167 164.494 398.449L164.532 398.475C164.996 398.784 165.299 398.969 165.688 398.969 165.887 398.969 166.11 398.919 166.385 398.819 166.623 398.729 166.858 398.63 167.089 398.522 167.158 398.49 167.228 398.459 167.303 398.427 167.979 398.124 168.82 397.781 169.844 397.781 170.172 397.781 170.438 398.047 170.438 398.375L170.438 405.5C170.438 405.828 170.172 406.094 169.844 406.094 169.086 406.094 168.443 406.343 167.785 406.636L167.596 406.72C167.335 406.838 167.058 406.963 166.791 407.06 166.439 407.198 166.065 407.273 165.688 407.281 164.912 407.281 164.335 406.895 163.912 406.614L163.874 406.588C163.41 406.279 163.107 406.094 162.719 406.094 161.73 406.094 161.272 406.386 160.079 407.181 159.806 407.363 159.437 407.289 159.256 407.016 159.191 406.919 159.156 406.804 159.156 406.688L159.156 399.562C159.156 399.364 159.256 399.179 159.421 399.069L159.469 399.038Z" fill-rule="evenodd"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="155" y="477" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-155 -477)"><rect x="155" y="477" width="19" height="19" fill="#92D050"/><path d="M159.156 478.188C159.484 478.188 159.75 478.453 159.75 478.781L159.75 494.219C159.75 494.547 159.484 494.812 159.156 494.812 158.828 494.812 158.562 494.547 158.562 494.219L158.562 478.781C158.562 478.453 158.828 478.188 159.156 478.188Z" fill-rule="evenodd"/><path d="M159.467 480.038C160.623 479.267 161.351 478.781 162.719 478.781 163.494 478.781 164.071 479.167 164.494 479.449L164.532 479.475C164.996 479.784 165.299 479.969 165.688 479.969 165.887 479.969 166.11 479.919 166.385 479.819 166.623 479.729 166.858 479.63 167.089 479.522 167.158 479.49 167.228 479.459 167.303 479.427 167.979 479.124 168.82 478.781 169.844 478.781 170.172 478.781 170.438 479.047 170.438 479.375L170.438 486.5C170.438 486.828 170.172 487.094 169.844 487.094 169.086 487.094 168.443 487.343 167.785 487.636L167.596 487.72C167.335 487.838 167.058 487.963 166.791 488.06 166.439 488.198 166.065 488.273 165.688 488.281 164.912 488.281 164.335 487.895 163.912 487.614L163.874 487.588C163.41 487.279 163.107 487.094 162.719 487.094 161.73 487.094 161.272 487.386 160.079 488.181 159.806 488.363 159.437 488.289 159.256 488.016 159.191 487.919 159.156 487.804 159.156 487.688L159.156 480.562C159.156 480.364 159.256 480.179 159.421 480.069L159.469 480.038Z" fill-rule="evenodd"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="-0 0 228 228" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="128" width="228" height="228"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -128)"><rect x="159" y="128" width="228" height="228" fill="#FF0000"/><path d="M273 128C210.04 128 159 179.04 159 242 159 304.961 210.04 356 273 356 335.961 356 387 304.961 387 242 387 179.04 335.961 128 273 128ZM287.668 313.763 268.288 313.763 268.288 184.335 267.851 184.335 242.087 198.3 238.287 183.1 270.587 165.772 287.687 165.772Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 705 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="161" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -161)"><rect x="159" y="161" width="19" height="19" fill="#946CC4"/><path d="M168.5 161C163.253 161 159 165.253 159 170.5 159 175.747 163.253 180 168.5 180 173.747 180 178 175.747 178 170.5 178 165.253 173.747 161 168.5 161ZM172.36 176.48 164.48 176.48 164.48 175.454 165.791 174.188C168.943 171.189 170.368 169.596 170.383 167.732 170.511 166.524 169.635 165.44 168.426 165.312 168.264 165.295 168.1 165.296 167.938 165.315 166.97 165.351 166.044 165.719 165.316 166.358L164.784 165.182C165.765 164.365 167.002 163.921 168.278 163.928 170.188 163.757 171.875 165.168 172.045 167.078 172.058 167.224 172.062 167.371 172.056 167.517 172.056 169.795 170.405 171.634 167.805 174.145L166.817 175.055 166.817 175.092 172.359 175.092Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="194" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -194)"><rect x="159" y="194" width="19" height="19" fill="#63E0E0"/><path d="M168.5 194C163.253 194 159 198.253 159 203.5 159 208.747 163.253 213 168.5 213 173.747 213 178 208.747 178 203.5 178 198.253 173.747 194 168.5 194ZM168.247 209.688C167.121 209.702 166.013 209.414 165.037 208.853L165.492 207.563C166.321 208.053 167.263 208.321 168.226 208.34 170.372 208.34 171.036 206.974 171.018 205.948 170.997 204.22 169.434 203.479 167.827 203.479L166.898 203.479 166.898 202.233 167.829 202.233C169.043 202.233 170.581 201.6 170.581 200.145 170.581 199.158 169.947 198.285 168.418 198.285 167.533 198.31 166.675 198.593 165.949 199.1L165.512 197.885C166.472 197.26 167.594 196.93 168.739 196.936 171.171 196.936 172.271 198.38 172.271 199.879 172.242 201.245 171.313 202.426 169.993 202.776L169.993 202.816C171.569 203.044 172.74 204.391 172.748 205.983 172.748 207.962 171.208 209.688 168.247 209.688Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="159" y="229" width="19" height="19"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-159 -229)"><rect x="159" y="229" width="19" height="19" fill="#92D050"/><path d="M167.992 235.574 164.972 239.767 164.972 239.806 169.018 239.806 169.018 235.69C169.018 235.044 169.037 234.398 169.075 233.753L169.018 233.753C168.638 234.474 168.342 235.004 167.992 235.574Z"/><path d="M168.5 229C163.253 229 159 233.253 159 238.5 159 243.747 163.253 248 168.5 248 173.747 248 178 243.747 178 238.5 178 233.253 173.747 229 168.5 229ZM172.322 241.119 170.593 241.119 170.593 244.48 169.01 244.48 169.01 241.119 163.283 241.119 163.283 240.01 168.79 232.137 170.593 232.137 170.593 239.809 172.322 239.809Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 936 B |
BIN
node/frontend-react/src/assets/images/ogure.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
14
node/frontend-react/src/components/Commun/Chargement.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Composant indiquant qu'un chargement est en cours (3 disques animés).
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
export const Chargement = () => (
|
||||
<div className="Chargement container-fluid vertical-center">
|
||||
<div className="circle" />
|
||||
<div className="circle" />
|
||||
<div className="circle" />
|
||||
</div>
|
||||
);
|
||||
21
node/frontend-react/src/components/Commun/Erreur.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Composant permettant d'indiquer qu'une erreur est survenue.
|
||||
*
|
||||
* @component
|
||||
* @returns
|
||||
*/
|
||||
export const Erreur = () => (
|
||||
<div className={'container-fluid vertical-center'}>
|
||||
<div className="jumbotron">
|
||||
<h1 className="display-4">Une erreur est survenue!</h1>
|
||||
<p className="lead">Veuillez contacter un administrateur.</p>
|
||||
<hr />
|
||||
|
||||
<a className="btn btn-primary" href="/accounts/login">
|
||||
Cliquez ici pour vous reconnecter.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,173 @@
|
||||
.Backdrop {
|
||||
position: fixed;
|
||||
z-index: 1040;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.FicheDetailleeModal {
|
||||
position: fixed;
|
||||
width: 90vw;
|
||||
max-width: 1400px;
|
||||
height: 90vh;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1041;
|
||||
border: 1px solid #e5e5e5;
|
||||
background-color: white;
|
||||
overflow: auto;
|
||||
|
||||
.FicheDetailleeHeader {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
background-color: #ddd;
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ExitButton {
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
font-size: 1.2em;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.FicheDetailleeBody {
|
||||
padding: 0.5rem;
|
||||
|
||||
h5 {
|
||||
margin: 1em 0 0 0;
|
||||
color: #0b1b7f;
|
||||
font-size: 1.15em;
|
||||
/*
|
||||
border-bottom: 1px solid #a5acda;
|
||||
padding: 0 0 2px 0;
|
||||
*/
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.3rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.InfosPerso {
|
||||
margin-bottom: 1em;
|
||||
|
||||
> h5:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.TabPanel {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.InfoColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.InfoRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.InfoDisplay {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.InfoDisplay {
|
||||
margin-top: 0.3rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.ChampHeader {
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 1.5rem;
|
||||
color: #0b1b7f;
|
||||
}
|
||||
|
||||
.ChampTexte {
|
||||
border: 1px solid #ddd;
|
||||
height: 7vh;
|
||||
overflow: auto;
|
||||
padding: 0.3em;
|
||||
}
|
||||
}
|
||||
.administreNotation {
|
||||
margin: 0.5em 0 0 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.25em 0.5em;
|
||||
border: 1px solid #999;
|
||||
text-align: center !important;
|
||||
vertical-align: middle;
|
||||
|
||||
&.vide {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
tbody {
|
||||
th {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactHtmlParser from 'react-html-parser';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
import { Modal } from 'react-overlays';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import 'react-tabs/style/react-tabs.css';
|
||||
import useSWR from 'swr';
|
||||
import i18n from '../../../i18n/i18n';
|
||||
import AppConfigStore from '../../../stores/AppConfigStore';
|
||||
import { Chargement } from '../Chargement';
|
||||
import { Erreur } from '../Erreur';
|
||||
import './FicheDetaillee.scss';
|
||||
import { ComplexDateDisplay, InfoDisplay } from './ficheDetailleeHelpers';
|
||||
import { mapDataAdministreForFicheDetaillee } from './ficheDetailleeMappingAdministre';
|
||||
|
||||
/**
|
||||
* Composant présentant la fiche détaillée d'un administré.
|
||||
*
|
||||
* @component
|
||||
* @param {array} props.appConfig - Le contexte d'affichage de la liste avec les données de références et l'administré affiché
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <FicheDetailleeAdministre appConfig={appConfig} />
|
||||
* )
|
||||
*/
|
||||
const FicheDetailleeAdministre = observer((props) => {
|
||||
/** @type {AppConfigStore} */
|
||||
const appConfig = props.appConfig;
|
||||
|
||||
let today = new Date(Date.now());
|
||||
let datePAMActuel = new Date('August 1, ' + today.getFullYear());
|
||||
const anneePAM = today.getTime() > datePAMActuel.getTime() ? datePAMActuel.getFullYear() + 1 : datePAMActuel.getFullYear();
|
||||
const datePAM = '01/08/' + anneePAM.toString();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const annee = appConfig.PAMSelectionne;
|
||||
const params = {};
|
||||
annee && (params.pam__in = annee);
|
||||
const getKey = () => appConfig.managers.administres.buildFicheDetailleeCallKey(appConfig.administreFicheDetaillee, params);
|
||||
const { data, error } = useSWR(getKey);
|
||||
const [administreDetails, setAdministreDetails] = useState(undefined);
|
||||
|
||||
const CHAMP_VIDE = i18n.global.donnees.vide;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const ad = mapDataAdministreForFicheDetaillee(appConfig.donneesReference, data);
|
||||
setAdministreDetails(ad);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const resetDetailsAndCloseModal = () => {
|
||||
appConfig.setAdministreFicheDetaillee(undefined);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const Backdrop = () => {
|
||||
return <div className="Backdrop" onClick={() => resetDetailsAndCloseModal()} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="FicheDetailleeModal" show={isOpen} renderBackdrop={Backdrop} /*enforceFocus={false}*/>
|
||||
<>
|
||||
{/* FIXME fixer le titre et le bouton exit en haut de la modale (ne fonctionne pas avec css fixed) */}
|
||||
<div className="FicheDetailleeHeader">
|
||||
{administreDetails && (
|
||||
<h3>
|
||||
{administreDetails.entete_identiteMatricule + ' '} —
|
||||
{' ' +
|
||||
administreDetails.entete_identiteGrade +
|
||||
' ' +
|
||||
administreDetails.entete_identiteNom +
|
||||
' ' +
|
||||
administreDetails.entete_identitePrenom}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
<div className="ExitButton" onClick={() => resetDetailsAndCloseModal()}>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="FicheDetailleeBody">
|
||||
{!administreDetails && (error ? <Erreur /> : <Chargement />)}
|
||||
{administreDetails && (
|
||||
<>
|
||||
<div className="InfosPerso">
|
||||
<h5>Informations personnelles</h5>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'SAP'} text={administreDetails.entete_identiteMatricule} />
|
||||
<InfoDisplay label={'IdDef'} text={administreDetails.entete_idDef} />
|
||||
<InfoDisplay label={'Date Jour'} text={dayjs(Date.now()).format('DD/MM/YYYY')} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Grade'} text={administreDetails.entete_identiteGrade} />
|
||||
<InfoDisplay label={'Date de naissance'} text={administreDetails.entete_identiteDateNaissance} />
|
||||
<InfoDisplay label={'Date PAM'} text={datePAM} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay
|
||||
label={'Nom/Prénom'}
|
||||
text={administreDetails.entete_identiteNom + ' ' + administreDetails.entete_identitePrenom}
|
||||
/>
|
||||
<InfoDisplay
|
||||
label={'Age'}
|
||||
text={
|
||||
administreDetails.entete_identiteAge > 1
|
||||
? administreDetails.entete_identiteAge + ' ans'
|
||||
: administreDetails.entete_identiteAge + ' an'
|
||||
}
|
||||
/>
|
||||
<InfoDisplay label={'Année A'} text={dayjs(Date.now()).format('YYYY')} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Arme'} text={administreDetails.entete_arme} />
|
||||
<InfoDisplay
|
||||
label={'Sexe'}
|
||||
text={i18n.administreDetails.sexe[administreDetails.entete_identiteSexe || 'NC']}
|
||||
/>
|
||||
<InfoDisplay label={'Millésime'} text={administreDetails.formob_millesime} />
|
||||
</div>
|
||||
<div className="InfoColumn" style={{ justifyContent: 'flex-end' }}>
|
||||
<InfoDisplay label={'Marquant PN'} text={administreDetails.entete_marqueur_pn} />
|
||||
<InfoDisplay label={'NID'} text={administreDetails.entete_NID} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5>Echéances</h5>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<ComplexDateDisplay
|
||||
label={'Date grade'}
|
||||
type={'Temps'}
|
||||
date={administreDetails.entete_dateGrade}
|
||||
temps={administreDetails.entete_tempsDateGrade}
|
||||
/>
|
||||
<ComplexDateDisplay
|
||||
label={'Date entrée service'}
|
||||
type={'Temps'}
|
||||
date={administreDetails.entete_dateEntreeService}
|
||||
temps={administreDetails.entete_tempsEntreeService}
|
||||
/>
|
||||
<ComplexDateDisplay
|
||||
label={'Date RDC'}
|
||||
type={'Reste'}
|
||||
date={administreDetails.entete_dateRDC}
|
||||
temps={administreDetails.entete_tempsRDC}
|
||||
/>
|
||||
<ComplexDateDisplay
|
||||
label={'Date entrée FE'}
|
||||
type={'Temps'}
|
||||
date={administreDetails.entete_dateEntreeFE}
|
||||
temps={administreDetails.entete_tempsDateEntreeFE}
|
||||
auPAM={anneePAM}
|
||||
/>
|
||||
<ComplexDateDisplay
|
||||
label={'Date dernier ACR'}
|
||||
type={'Temps'}
|
||||
date={administreDetails.entete_dateDernierACR}
|
||||
temps={administreDetails.entete_tempsDernierACR}
|
||||
auPAM={anneePAM}
|
||||
/>
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Recrutement'} text={administreDetails.entete_recrutement} />
|
||||
<InfoDisplay label={'Interruption de service'} text={administreDetails.entete_dateInterruptionService} />
|
||||
<InfoDisplay label={'Corps Statut'} text={administreDetails.entete_corpsStatut} />
|
||||
<InfoDisplay label={'Compétences'} text={administreDetails.entete_competences} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Emploi occupé'} text={administreDetails.entete_emploiOccupe} />
|
||||
<InfoDisplay label={'EIP'} text={administreDetails.entete_EIP} />
|
||||
<InfoDisplay label={'EIS'} text={administreDetails.entete_EIS} />
|
||||
<InfoDisplay label={'PLS GB Max'} text={administreDetails.entete_PLSGBMax} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>FORMOB/FEMP</Tab>
|
||||
<Tab>Affectations/Fonctions</Tab>
|
||||
<Tab>Famille</Tab>
|
||||
<Tab>Diplômes</Tab>
|
||||
<Tab>FUD</Tab>
|
||||
<Tab>Notation</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<div className={'TabPanel'}>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Départements souhaités</h5>
|
||||
{administreDetails.formob_departementsSouhaites.length
|
||||
? administreDetails.formob_departementsSouhaites.map((fe, index) => (
|
||||
<InfoDisplay key={nanoid()} label={index + 1} text={fe} />
|
||||
))
|
||||
: CHAMP_VIDE}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Fonction d'emploi</h5>
|
||||
{administreDetails.formob_cinqFonctionsEmploi.map((fe, index) => (
|
||||
<InfoDisplay key={nanoid()} label={index + 1} text={fe} />
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Commune d'emploi</h5>
|
||||
{administreDetails.formob_cinqCommunesEmploi.map((fe, index) => (
|
||||
<InfoDisplay key={nanoid()} label={index + 1} text={fe} />
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Priorisation</h5>
|
||||
{administreDetails.formob_cinqPriorisationsEmploi.map((fe, index) => (
|
||||
<InfoDisplay key={nanoid()} label={index + 1} text={fe} />
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Commentaires de la DRHAT</h5>
|
||||
<div className="ChampTexte" style={{ height: '8.5em', width: '15vw' }}>
|
||||
{ReactHtmlParser(administreDetails.formob_commentaireDRHAT)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Soldat</h5>
|
||||
<InfoDisplay label={'Spécialité'} text={administreDetails.formob_specialiteSoldat} />
|
||||
<InfoDisplay label={'CIAT'} text={administreDetails.formob_CIATSoldat} />
|
||||
<InfoDisplay label={'Int Bassin'} text={administreDetails.formob_intBassinSoldat} />
|
||||
<InfoDisplay label={'Ext Bassin'} text={administreDetails.formob_extBassinSoldat} />
|
||||
<InfoDisplay label={'TSHM'} text={administreDetails.formob_TSHMSoldat} />
|
||||
<InfoDisplay label={'Rec Particulier'} text={administreDetails.formob_recParticulierSoldat} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Commandement</h5>
|
||||
<InfoDisplay label={'Spécialité'} text={administreDetails.formob_specialiteCommandemant} />
|
||||
<InfoDisplay label={'CIAT'} text={administreDetails.formob_CIATCommandemant} />
|
||||
<InfoDisplay label={'Int Bassin'} text={administreDetails.formob_intBassinCommandemant} />
|
||||
<InfoDisplay label={'Ext Bassin'} text={administreDetails.formob_extBassinCommandemant} />
|
||||
<InfoDisplay label={'TSHM'} text={administreDetails.formob_TSHMCommandemant} />
|
||||
<InfoDisplay label={'Rec Particulier'} text={administreDetails.formob_recParticulierCommandemant} />
|
||||
<InfoDisplay label={'CDC FAV Mutation'} text={administreDetails.formob_CDCFAVMutationCommandemant} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Remarques de l'intéressé</h5>
|
||||
<div className="ChampTexte" style={{ height: '7em', width: '20vw' }}>
|
||||
{ReactHtmlParser(administreDetails.formob_remarqueInteresse)}
|
||||
</div>
|
||||
<h5 className="ChampHeader">Avis du commandement</h5>
|
||||
<div className="ChampTexte" style={{ height: '7em', width: '20vw' }}>
|
||||
{ReactHtmlParser(administreDetails.formob_avisCommandemant)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="InfoRow">
|
||||
<InfoDisplay
|
||||
label={"OBS ou Prévision d'affectation"}
|
||||
text={administreDetails.formob_OBSouPrevisionAffectation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className={'TabPanel'}>
|
||||
<div className="InfoRow">
|
||||
<InfoDisplay label={'Le'} text={dayjs(Date.now()).format('DD/MM/YYYY')} />
|
||||
<InfoDisplay label={'Credo'} text={administreDetails.affectationsfonctions_credo} />
|
||||
<InfoDisplay label={'Garnison'} text={administreDetails.affectationsfonctions_garnison} />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Historique des Affectations</h5>
|
||||
{administreDetails.affectations_historique.map((a, index) => (
|
||||
<InfoDisplay key={nanoid()} label={index + 1} text={a} />
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Date des Affectations</h5>
|
||||
{administreDetails.affectations_date_historique.map((a, index) => (
|
||||
<p key={nanoid()}>{a ? dayjs(a).format('DD/MM/YYYY') : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Historique des Fonctions</h5>
|
||||
{administreDetails.fonctions_historique.map((a, index) => (
|
||||
<InfoDisplay key={nanoid()} label={index + 1} text={a} />
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<h5 className="ChampHeader">Date des Fonctions</h5>
|
||||
{administreDetails.fonctions_date_historique.map((a, index) => (
|
||||
<p key={nanoid()}>{a ? dayjs(a).format('DD/MM/YYYY') : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<InfoDisplay
|
||||
label={'Position statuaire au '}
|
||||
text={administreDetails.affectationsfonctions_datePositionStatuaire}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className={'TabPanel'}>
|
||||
<h5 className="ChampHeader">Famille</h5>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Situation familiale'} text={administreDetails.famille_situation} />
|
||||
<InfoDisplay label={'Profession du conjoint'} text={administreDetails.famille_professionConjoint} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Date'} text={administreDetails.famille_date} />
|
||||
<InfoDisplay label={'Sexe du conjoint'} text={administreDetails.famille_sexeConjoint} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={"Nombre d'enfants"} text={administreDetails.famille_nbEnfants} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={"Info sur les enfants"} text={administreDetails.famille_infoEnfants} />
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="ChampHeader">Complément conjoint</h5>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Grade'} text={administreDetails.famille_gradeConjoint} />
|
||||
<InfoDisplay label={'Nom'} text={administreDetails.famille_nomConjoint} />
|
||||
<InfoDisplay label={'Prénom'} text={administreDetails.famille_prenomConjoint} />
|
||||
<InfoDisplay label={'SAP'} text={administreDetails.famille_SAPConjoint} />
|
||||
<InfoDisplay label={'NID'} text={administreDetails.famille_NIDConjoint} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'CREDO FE'} text={administreDetails.famille_credoFEConjoint} />
|
||||
<InfoDisplay label={'FE'} text={administreDetails.famille_FEConjoint} />
|
||||
<InfoDisplay label={'Garnison'} text={administreDetails.famille_garnisonConjoint} />
|
||||
<InfoDisplay label={'Fmobé'} text={administreDetails.famille_fmobeConjoint} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className={'TabPanel'}>
|
||||
<div className="InfoRow">
|
||||
<InfoDisplay
|
||||
label={'Diplôme militaire le plus haut'}
|
||||
text={administreDetails.diplomes_diplMiliLePlusHaut}
|
||||
/>
|
||||
<InfoDisplay label={'Dernier diplôme militaire'} text={administreDetails.diplomes_dernierDiplMili} />
|
||||
</div>
|
||||
<hr />
|
||||
<h5 className="ChampHeader">Diplômes militaires (10 derniers)</h5>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<p>
|
||||
<strong>Diplômes militaires</strong>
|
||||
</p>
|
||||
{administreDetails.diplomes_dixDerniersDiplMili.map((d, index) => (
|
||||
<p key={nanoid()}>{d ? d : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<p>
|
||||
<strong>Date</strong>
|
||||
</p>
|
||||
{administreDetails.diplomes_dixDatesDerniersDipl.map((d, index) => (
|
||||
<p key={nanoid()}>{d ? dayjs(d).format('DD/MM/YYYY') : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<p>
|
||||
<strong>Note</strong>
|
||||
</p>
|
||||
{administreDetails.diplomes_dixNotesDerniersDipl.map((d, index) => (
|
||||
<p key={nanoid()}>{d || d === 0.0 ? d : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className={'TabPanel'}>
|
||||
<div className="InfoRow">
|
||||
<div className="InfoColumn">
|
||||
<p>
|
||||
<strong>Demande</strong>
|
||||
</p>
|
||||
{administreDetails.fud_dixLibelle.map((d, index) => (
|
||||
<p key={nanoid()}>{d ? d : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<p>
|
||||
<strong>Date début</strong>
|
||||
</p>
|
||||
{administreDetails.fud_dixDatesDebut.map((d, index) => (
|
||||
<p key={nanoid()}>{d ? dayjs(d).format('DD/MM/YYYY') : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<p>
|
||||
<strong>Date Fin</strong>
|
||||
</p>
|
||||
{administreDetails.fud_dixDatesFin.map((d, index) => (
|
||||
<p key={nanoid()}>{d ? dayjs(d).format('DD/MM/YYYY') : i18n.global.donnees.vide}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="InfoRow">
|
||||
<InfoDisplay label={'Lien en service'} text={administreDetails.lien_en_service} />
|
||||
</div>
|
||||
<hr />
|
||||
<table className="administreNotation">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Année</th>
|
||||
{administreDetails.anne_de_notation.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Age</th>
|
||||
{administreDetails.no_age_annees.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IRIS</th>
|
||||
{administreDetails.a_iris.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IRIS cumulé</th>
|
||||
{administreDetails.a_iris_cumule.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>NR/NGC</th>
|
||||
{administreDetails.a_ngc_cumule.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>RAC</th>
|
||||
{administreDetails.a_rac.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>RF/QSR</th>
|
||||
{administreDetails.a_rf_qsr.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Apt resp / Emp</th>
|
||||
{administreDetails.a_aptitude_emploi_sup.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Potentiel CAT Sup</th>
|
||||
{administreDetails.a_potentiel_responsabilite_sup.map((ap) => (
|
||||
<td>{ap}</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default FicheDetailleeAdministre;
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import i18n from '../../../i18n/i18n';
|
||||
import MarquesRenderer from '../renderers/MarquesRenderer';
|
||||
|
||||
/**
|
||||
* Librairie de fonctions et composants utilitaires nécessaires à l'affichage de la fiche détaillée d'un administré.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Affiche une info.
|
||||
*/
|
||||
export const InfoDisplay = observer((props) => {
|
||||
return (
|
||||
<div className="InfoDisplay">
|
||||
<strong>{props.label} : </strong>{' '}
|
||||
{props.text && props.text.toString().length ? props.text : <span className="donneeAbsente">{i18n.global.donnees.vide}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Affiche une date avec plusieurs paramètres.
|
||||
*/
|
||||
export const ComplexDateDisplay = observer((props) => {
|
||||
const CHAMP_VIDE = i18n.global.donnees.vide;
|
||||
const jourPluriel = props.temps !== '' ? (props.temps > 1 ? ' ans' : ' an') : '? ans';
|
||||
const temps = parseInt(props.temps, 10) < 0 ? 0 : props.temps;
|
||||
const toDisplay =
|
||||
props.type == 'Temps'
|
||||
? props.date + (props.date && props.date != CHAMP_VIDE ? ' (depuis ' + temps + jourPluriel + ')' : '')
|
||||
: props.date + (props.date && props.date != CHAMP_VIDE ? ' (reste ' + temps + jourPluriel + ')' : '');
|
||||
|
||||
return (
|
||||
<div className="InfoDisplay">
|
||||
<strong>{props.label} : </strong> {props.date ? toDisplay : <span className="donneeAbsente">{i18n.global.donnees.vide}</span>}
|
||||
{props.auPAM && (
|
||||
<>
|
||||
<strong> / au PAM : </strong>
|
||||
{props.auPAM}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const MarquesRendererDisplay = observer((props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="MarquesRendererDisplay">
|
||||
<strong>{props.label} :</strong>
|
||||
<div
|
||||
onClick={() => {
|
||||
alert("TEST");
|
||||
//setIsMarquesEditorOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.marques && props.marques.length ? (
|
||||
<MarquesRenderer marques={props.marques} modeFiltre={false} context={props.context} />
|
||||
) : (
|
||||
<span>{i18n.global.donnees.vide}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* @component
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { COMPETENCES_SEPARATEUR, MARQUES_SEPARATEUR, DEPARTEMENTS_SOUHAITES_SEPARATEUR } from '../../../constantes/constantes';
|
||||
import i18n from '../../../i18n/i18n';
|
||||
import { getDepartements } from '../../Commun/listes/helpers/business-data-helpers';
|
||||
|
||||
/**
|
||||
* Adapte les données d'UN administré reçues du backend, pour l'affichage dans la fiche détaillée :
|
||||
* - Mapping des champs
|
||||
* - Ajout de valeur par défaut pour les données manquantes (généralement 'A récupérer BDD')
|
||||
* - Ajout de certains champs calculés (ex : identite_age, service_temps)
|
||||
*
|
||||
* @param {Object} donneesReference - Données de référence
|
||||
* @param {<Object>} a - Objet contenant les données issues du backend pour un administré
|
||||
* @returns {<Object>} Objet normalisé et complété, pour affichage dans la fiche détaillée
|
||||
*/
|
||||
export function mapDataAdministreForFicheDetaillee(donneesReference, a) {
|
||||
const now = dayjs();
|
||||
const anneeCourante = now.toDate().getUTCFullYear();
|
||||
const CHAMP_VIDE = i18n.global.donnees.vide;
|
||||
|
||||
//console.log('administré detaillé', a);
|
||||
|
||||
return {
|
||||
// Entête
|
||||
entete_identiteMatricule: a.a_id_sap || CHAMP_VIDE,
|
||||
entete_identiteGrade: a.grade || CHAMP_VIDE,
|
||||
entete_identiteNom: a.a_nom || CHAMP_VIDE,
|
||||
entete_identitePrenom: a.a_prenom || CHAMP_VIDE,
|
||||
entete_identiteDateNaissance: a.a_date_naissance ? dayjs(a.a_date_naissance).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
entete_identiteAge: a.a_date_naissance ? anneeCourante - new Date(a.a_date_naissance).getUTCFullYear() : CHAMP_VIDE,
|
||||
entete_identiteSexe: a.a_sexe || CHAMP_VIDE,
|
||||
entete_idDef: a.a_id_def || CHAMP_VIDE,
|
||||
entete_dateGrade: a.a_grade_date_debut ? dayjs(a.a_grade_date_debut).format('DD/MM/YYYY') : CHAMP_VIDE, //TODO: champ a_date_grade manquant
|
||||
entete_tempsDateGrade: a.a_grade_date_debut ? dayjs(now).diff(a.a_grade_date_debut, 'year') : CHAMP_VIDE, //TODO: champ a_date_grade manquant
|
||||
entete_dateEntreeFE: a.a_date_arrivee_fe ? dayjs(a.a_date_arrivee_fe).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
entete_tempsDateEntreeFE: a.a_date_arrivee_fe ? dayjs(now).diff(a.a_date_arrivee_fe, 'year') : CHAMP_VIDE,
|
||||
entete_dateInterruptionService: a.a_interruption_service, //? dayjs(a.a_interruption_service).format('DD/MM/YYYY') : CHAMP_VIDE, //TODO champ a_interruption_service au mauvais format (ex :"0A 0M 0J")
|
||||
entete_dateEntreeService: a.a_date_entree_service ? dayjs(a.a_date_entree_service).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
entete_tempsEntreeService: a.a_date_entree_service ? dayjs(now).diff(a.a_date_entree_service, 'year') : CHAMP_VIDE,
|
||||
entete_dateRDC: a.a_date_rdc ? dayjs(a.a_date_rdc).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
entete_tempsRDC: a.a_date_rdc ? dayjs(a.a_date_rdc).diff(now, 'year') : CHAMP_VIDE,
|
||||
entete_dateDernierACR: a.a_date_dernier_acr ? dayjs(a.a_date_dernier_acr).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
entete_tempsDernierACR: a.a_date_dernier_acr ? dayjs(now).diff(a.a_date_dernier_acr, 'year') : CHAMP_VIDE,
|
||||
entete_arme: a.a_arme || CHAMP_VIDE,
|
||||
entete_recrutement: a.a_rg_origine_recrutement || CHAMP_VIDE,
|
||||
entete_corpsStatut: a.a_pos_statuaire || CHAMP_VIDE,
|
||||
entete_competences: a.a_liste_id_competences ? a.a_liste_id_competences :[], // liste des compétences du soldat
|
||||
entete_emploiOccupe: a.a_fonction || CHAMP_VIDE,
|
||||
entete_EIP: a.a_eip_fiche_detaille || CHAMP_VIDE,
|
||||
entete_EIS: a.a_eis || CHAMP_VIDE,
|
||||
entete_BG: a.a_bg || CHAMP_VIDE, // TODO champ a_bg manquant
|
||||
entete_PLSGBMax: a.a_pls_gb_max || CHAMP_VIDE,
|
||||
entete_marqueur_pn: a.a_marqueur_pn || CHAMP_VIDE,
|
||||
entete_NID : a.a_id_def || CHAMP_VIDE,
|
||||
|
||||
// FORMOB_FOREMP
|
||||
formob_millesime: a.fmob ? a.fmob.fmob_millesime : anneeCourante, // liste des 5 fonctions emploi
|
||||
formob_cinqFonctionsEmploi: a.fmob
|
||||
? [a.fmob.fmob_fonction_1, a.fmob.fmob_fonction_2, a.fmob.fmob_fonction_3, a.fmob.fmob_fonction_4, a.fmob.fmob_fonction_5]
|
||||
: [], // liste des 5 fonctions emploi
|
||||
formob_cinqCommunesEmploi: a.fmob
|
||||
? [a.fmob.fmob_commune_1, a.fmob.fmob_commune_2, a.fmob.fmob_commune_3, a.fmob.fmob_commune_4, a.fmob.fmob_commune_5]
|
||||
: [], // liste des 5 communes emploi
|
||||
formob_cinqPriorisationsEmploi: a.fmob
|
||||
? [a.fmob.fmob_prio_1, a.fmob.fmob_prio_2, a.fmob.fmob_prio_3, a.fmob.fmob_prio_4, a.fmob.fmob_prio_5]
|
||||
: [], // liste des 5 priorisations emploi
|
||||
formob_departementsSouhaites: a.a_liste_depts_souhaites
|
||||
? getDepartements(donneesReference, a.a_liste_depts_souhaites.split(DEPARTEMENTS_SOUHAITES_SEPARATEUR)).map(
|
||||
(d) => d.code + (d.code != d.libelle ? ' (' + d.libelle + ')' : '')
|
||||
)
|
||||
: [], // liste des départements souhaités
|
||||
|
||||
formob_commentaireDRHAT: a.fmob ? a.fmob.fmob_commentaire_ac : CHAMP_VIDE, //TODO : non retrouvé dans la db formob
|
||||
formob_CIATSoldat: a.fmob ? (a.fmob.fmob_mobilite_centre_interet_adt ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_intBassinSoldat: a.fmob ? (a.fmob.fmob_mobilite_bassin_interne ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_extBassinSoldat: a.fmob ? (a.fmob.fmob_mobilite_bassin_externe ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_TSHMSoldat: a.fmob ? (a.fmob.fmob_mobilite_hors_metropole ? 'Oui' : 'Non') : CHAMP_VIDE, //TODO: TSHM contient hors metropole ?
|
||||
formob_specialiteSoldat: a.fmob ? (a.fmob.fmob_mobilite_dans_specialite ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_recParticulierSoldat: a.fmob ? (a.fmob.fmob_mobilite_recrutement_particulier_administre ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
//TODO: Commandement
|
||||
// la partie du FORMOB en lien avec l'avis du commandement
|
||||
formob_specialiteCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mobilite_specialite ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_CIATCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mobilite_centre_interet ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_intBassinCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mobilite_interne ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_extBassinCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mobilite_externe ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_TSHMCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mobilite_hors_metropole ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_recParticulierCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mobilite_recrutement_particulier_admin ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_CDCFAVMutationCommandemant: a.fmob ? (a.fmob.fmob_avis_cdc_mutation_administre ? 'Oui' : 'Non') : CHAMP_VIDE,
|
||||
formob_remarqueInteresse: a.fmob ? a.fmob.fmob_remarques_eventuelles_administres : CHAMP_VIDE,
|
||||
formob_avisCommandemant: a.fmob ? a.fmob.fmob_avis_commandant_formation : CHAMP_VIDE,
|
||||
formob_avisMutabilite: a.fmob ? a.fmob.fmob_avis_mutabilite : CHAMP_VIDE,
|
||||
formob_OBSouPrevisionAffectation: a.fmob ? a.fmob.fmob_obs : CHAMP_VIDE,
|
||||
formob_credoFEFuture: a.fmob ? a.fmob.fmob_fe_future : CHAMP_VIDE, // TODO: Comment on obtient cette donnée ?
|
||||
formob_marques: a.a_liste_id_marques ? a.a_liste_id_marques.split(MARQUES_SEPARATEUR) : [],
|
||||
|
||||
// Affectations/Fonctions
|
||||
affectationsfonctions_credo: a.a_credo_fe || CHAMP_VIDE,
|
||||
affectationsfonctions_garnison: a.fe ? a.fe.fe_garnison_lieu : CHAMP_VIDE,
|
||||
//TODO THomas, on en a 9
|
||||
affectations_historique: [
|
||||
a.a_affectation1,
|
||||
a.a_affectation2,
|
||||
a.a_affectation3,
|
||||
a.a_affectation4,
|
||||
a.a_affectation5,
|
||||
a.a_affectation6,
|
||||
a.a_affectation7,
|
||||
a.a_affectation8,
|
||||
a.a_affectation9
|
||||
] || [CHAMP_VIDE], // liste des 10 affectations
|
||||
affectations_date_historique: [
|
||||
a.a_date_affectation1,
|
||||
a.a_date_affectation2,
|
||||
a.a_date_affectation3,
|
||||
a.a_date_affectation4,
|
||||
a.a_date_affectation5,
|
||||
a.a_date_affectation6,
|
||||
a.a_date_affectation7,
|
||||
a.a_date_affectation8,
|
||||
a.a_date_affectation9
|
||||
] || [CHAMP_VIDE], // liste des 10 date affectations
|
||||
fonctions_historique: [
|
||||
a.a_fonction1,
|
||||
a.a_fonction2,
|
||||
a.a_fonction3,
|
||||
a.a_fonction4,
|
||||
a.a_fonction5,
|
||||
a.a_fonction6,
|
||||
a.a_fonction7,
|
||||
a.a_fonction8,
|
||||
a.a_fonction9
|
||||
] || [CHAMP_VIDE], // liste des 10 fonctions
|
||||
fonctions_date_historique: [
|
||||
a.a_date_fonction1,
|
||||
a.a_date_fonction2,
|
||||
a.a_date_fonction3,
|
||||
a.a_date_fonction4,
|
||||
a.a_date_fonction5,
|
||||
a.a_date_fonction6,
|
||||
a.a_date_fonction7,
|
||||
a.a_date_fonction8,
|
||||
a.a_date_fonction9
|
||||
] || [CHAMP_VIDE], // liste des 10 date fonctions
|
||||
affectationsfonctions_datePositionStatuaire: a.a_date_pos_statuaire ? dayjs(a.a_date_pos_statuaire).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
|
||||
// Famille
|
||||
famille_situation: a.a_situation_fam || CHAMP_VIDE,
|
||||
famille_date: a.a_date_mariage ? dayjs(a.a_date_mariage).format('DD/MM/YYYY') : CHAMP_VIDE,
|
||||
famille_nbEnfants: a.a_nombre_enfants.toString() || CHAMP_VIDE,
|
||||
famille_infoEnfants: a.a_enfants || CHAMP_VIDE,
|
||||
famille_professionConjoint: a.a_profession_conjoint || CHAMP_VIDE,
|
||||
famille_sexeConjoint: a.a_sexe_conjoint || CHAMP_VIDE,
|
||||
famille_gradeConjoint: a.conjoint ? a.conjoint.grade : CHAMP_VIDE,
|
||||
famille_nomConjoint: a.conjoint ? a.conjoint.a_nom : CHAMP_VIDE,
|
||||
famille_prenomConjoint: a.conjoint ? a.conjoint.a_prenom : CHAMP_VIDE,
|
||||
famille_SAPConjoint: a.a_sap_conjoint || CHAMP_VIDE,
|
||||
famille_NIDConjoint: a.conjoint ? a.conjoint.a_id_def : CHAMP_VIDE,
|
||||
famille_credoFEConjoint: a.conjoint ? a.conjoint.a_credo_fe : CHAMP_VIDE,
|
||||
famille_FEConjoint: a.conjoint ? (a.conjoint.fe ? a.conjoint.fe.fe_libelle : CHAMP_VIDE) : CHAMP_VIDE,
|
||||
famille_garnisonConjoint: a.conjoint ? (a.conjoint.fe ? a.conjoint.fe.fe_garnison_lieu : CHAMP_VIDE) : CHAMP_VIDE,
|
||||
famille_fmobeConjoint : a.conjoint ? a.conjoint.fmob_O_N : CHAMP_VIDE,
|
||||
|
||||
// Diplomes
|
||||
diplomes_diplMiliLePlusHaut: a.a_diplome_hl || CHAMP_VIDE,
|
||||
diplomes_dernierDiplMili: a.a_dernier_diplome || CHAMP_VIDE,
|
||||
diplomes_dixDerniersDiplMili: [
|
||||
a.a_diplome_1,
|
||||
a.a_diplome_2,
|
||||
a.a_diplome_3,
|
||||
a.a_diplome_4,
|
||||
a.a_diplome_5,
|
||||
a.a_diplome_6,
|
||||
a.a_diplome_7,
|
||||
a.a_diplome_8,
|
||||
a.a_diplome_9,
|
||||
a.a_diplome_10
|
||||
] || [CHAMP_VIDE], // liste des 10 derniers diplômes
|
||||
diplomes_dixDatesDerniersDipl: [
|
||||
a.a_diplome_1_date,
|
||||
a.a_diplome_2_date,
|
||||
a.a_diplome_3_date,
|
||||
a.a_diplome_4_date,
|
||||
a.a_diplome_5_date,
|
||||
a.a_diplome_6_date,
|
||||
a.a_diplome_7_date,
|
||||
a.a_diplome_8_date,
|
||||
a.a_diplome_9_date,
|
||||
a.a_diplome_10_date
|
||||
] || [CHAMP_VIDE], // liste des 10 dates des 10 derniers diplômes
|
||||
diplomes_dixNotesDerniersDipl: [
|
||||
a.a_diplome_1_note,
|
||||
a.a_diplome_2_note,
|
||||
a.a_diplome_3_note,
|
||||
a.a_diplome_4_note,
|
||||
a.a_diplome_5_note,
|
||||
a.a_diplome_6_note,
|
||||
a.a_diplome_7_note,
|
||||
a.a_diplome_8_note,
|
||||
a.a_diplome_9_note,
|
||||
a.a_diplome_10_note
|
||||
] || [CHAMP_VIDE], // liste des 10 notes des 10 derniers diplômes
|
||||
|
||||
fud_dixDatesDebut: [
|
||||
a.a_fud_1_dd,
|
||||
a.a_fud_2_dd,
|
||||
a.a_fud_3_dd,
|
||||
a.a_fud_4_dd,
|
||||
a.a_fud_5_dd,
|
||||
a.a_fud_6_dd,
|
||||
a.a_fud_7_dd,
|
||||
a.a_fud_8_dd,
|
||||
a.a_fud_9_dd,
|
||||
a.a_fud_10_dd
|
||||
] || [CHAMP_VIDE], // liste des 10 derniers diplômes
|
||||
fud_dixDatesFin: [
|
||||
a.a_fud_1_df,
|
||||
a.a_fud_2_df,
|
||||
a.a_fud_3_df,
|
||||
a.a_fud_4_df,
|
||||
a.a_fud_5_df,
|
||||
a.a_fud_6_df,
|
||||
a.a_fud_7_df,
|
||||
a.a_fud_8_df,
|
||||
a.a_fud_9_df,
|
||||
a.a_fud_10_df
|
||||
] || [CHAMP_VIDE], // liste des 10 dates des 10 derniers diplômes
|
||||
fud_dixLibelle: [
|
||||
a.a_fud_1_l,
|
||||
a.a_fud_2_l,
|
||||
a.a_fud_3_l,
|
||||
a.a_fud_4_l,
|
||||
a.a_fud_5_l,
|
||||
a.a_fud_6_l,
|
||||
a.a_fud_7_l,
|
||||
a.a_fud_8_l,
|
||||
a.a_fud_9_l,
|
||||
a.a_fud_10_l
|
||||
] || [CHAMP_VIDE] ,// liste des 10 notes des 10 derniers diplômes
|
||||
|
||||
|
||||
// Pour l'onglet Notation
|
||||
lien_en_service: a.a_lien_service || CHAMP_VIDE,
|
||||
|
||||
anne_de_notation: [
|
||||
parseInt(a.no_annne_de_notation_A_1) || CHAMP_VIDE,
|
||||
parseInt(a.no_annne_de_notation_A_2) || CHAMP_VIDE,
|
||||
parseInt(a.no_annne_de_notation_A_3) || CHAMP_VIDE,
|
||||
parseInt(a.no_annne_de_notation_A_4) || CHAMP_VIDE,
|
||||
parseInt(a.no_annne_de_notation_A_5) || CHAMP_VIDE,
|
||||
parseInt(a.no_annne_de_notation_A_6) || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE], // liste des 6 annees de notation
|
||||
|
||||
a_iris: a.a_categorie == 'OFF' ? [
|
||||
a.no_nr_ou_iris_A_1 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_2 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_3 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_4 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_5 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE] : [CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE], // liste des 6 NR/NGC
|
||||
|
||||
a_iris_cumule: a.a_categorie == 'OFF' ? [
|
||||
a.no_rac_ou_iris_cumule_A_1 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_2 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_3 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_4 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_5 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE] : [CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE], // liste des 6 NR/NGC
|
||||
|
||||
|
||||
a_ngc_cumule: a.a_categorie != 'OFF' ? [
|
||||
a.no_nr_ou_iris_A_1 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_2 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_3 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_4 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_5 || CHAMP_VIDE,
|
||||
a.no_nr_ou_iris_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE] : [CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE], // liste des 6 NR/NGC
|
||||
|
||||
a_rac: a.a_categorie != 'OFF' ? [
|
||||
a.no_rac_ou_iris_cumule_A_1 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_2 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_3 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_4 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_5 || CHAMP_VIDE,
|
||||
a.no_rac_ou_iris_cumule_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE] : [CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE,CHAMP_VIDE], // liste des 6 RAC
|
||||
|
||||
a_rf_qsr: [
|
||||
a.no_rf_qsr_A_1 || CHAMP_VIDE,
|
||||
a.no_rf_qsr_A_2 || CHAMP_VIDE,
|
||||
a.no_rf_qsr_A_3 || CHAMP_VIDE,
|
||||
a.no_rf_qsr_A_4 || CHAMP_VIDE,
|
||||
a.no_rf_qsr_A_5 || CHAMP_VIDE,
|
||||
a.no_rf_qsr_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE], // liste des 160 RF/QCR
|
||||
|
||||
a_aptitude_emploi_sup: [
|
||||
a.no_aptitude_emploie_sup_A_1 || CHAMP_VIDE,
|
||||
a.no_aptitude_emploie_sup_A_2 || CHAMP_VIDE,
|
||||
a.no_aptitude_emploie_sup_A_3 || CHAMP_VIDE,
|
||||
a.no_aptitude_emploie_sup_A_4 || CHAMP_VIDE,
|
||||
a.no_aptitude_emploie_sup_A_5 || CHAMP_VIDE,
|
||||
a.no_aptitude_emploie_sup_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE], // liste des 6 Apt resp / Emp
|
||||
|
||||
a_potentiel_responsabilite_sup: [
|
||||
a.no_potentiel_responsabilite_sup_A_1 || CHAMP_VIDE,
|
||||
a.no_potentiel_responsabilite_sup_A_2 || CHAMP_VIDE,
|
||||
a.no_potentiel_responsabilite_sup_A_3 || CHAMP_VIDE,
|
||||
a.no_potentiel_responsabilite_sup_A_4 || CHAMP_VIDE,
|
||||
a.no_potentiel_responsabilite_sup_A_5 || CHAMP_VIDE,
|
||||
a.no_potentiel_responsabilite_sup_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE], // liste des 6 Potentiel Cat. Sup
|
||||
|
||||
no_age_annees: [
|
||||
a.no_age_annees_A_1 || CHAMP_VIDE,
|
||||
a.no_age_annees_A_2 || CHAMP_VIDE,
|
||||
a.no_age_annees_A_3 || CHAMP_VIDE,
|
||||
a.no_age_annees_A_4 || CHAMP_VIDE,
|
||||
a.no_age_annees_A_5 || CHAMP_VIDE,
|
||||
a.no_age_annees_A_6 || CHAMP_VIDE,
|
||||
] || [CHAMP_VIDE], // liste des 6 Age
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import './InformationsFiltres.scss';
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
|
||||
/**
|
||||
* Composant permettant d'afficher le nombre d'éléments d'une liste AG Grid, le nombre d'éléments sélectionnés,
|
||||
* le nombre d'éléments filtrés et un lien d'action permettant de retirer tous les filtres.
|
||||
*
|
||||
* @component
|
||||
* @param {string} props.itemLabel - Libellé d'un élément au singulier (ex: "administré")
|
||||
* @param {string} props.itemsLabel - Libellé d'un élément au pluriel (ex: "administrés")
|
||||
* @param {number} props.nbItems - Le nombre total d'éléments dans la liste
|
||||
* @param {boolean} props.isFiltered - Indique si la liste est actuellement filtrée
|
||||
* @param {number} props.nbFiltered - Le nombre total d'éléments filtrés dans la liste
|
||||
* @param {number} props.nbSelected - Le nombre total d'éléments sélectionnés dans la liste
|
||||
* @param {number} props.nbSelectedVisible - Le nombre d'éléments sélectionnés visibles dans la liste (en cas de filtrage actif)
|
||||
*
|
||||
*/
|
||||
const InformationsFiltres = observer((props) => {
|
||||
const itemsLabel = props.nbItems <= 1 ? props.itemLabel : props.itemsLabel;
|
||||
|
||||
const selectedText =
|
||||
props.nbSelected >= 1 ? (
|
||||
<>
|
||||
{' '}
|
||||
dont <strong>{props.nbSelected}</strong> sélectionné{props.nbSelected > 1 ? 's' : ''}
|
||||
{props.nbSelectedVisible < props.nbSelected ? (
|
||||
<>
|
||||
{' '}
|
||||
<span className="avertissement">
|
||||
(<strong>{props.nbSelectedVisible}</strong> visible{props.nbSelectedVisible > 1 ? 's' : ''} après filtrage)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="InformationsFiltres ml-0" style={{ marginTop: '0.3em' }}>
|
||||
{props.isFiltered ? (
|
||||
<>
|
||||
<p className="m-0">
|
||||
Affichage filtré de
|
||||
<strong>{' ' + props.nbFiltered + ' ' + itemsLabel + ' '}</strong>
|
||||
sur
|
||||
<strong>{' ' + props.nbItems}</strong> au total{selectedText}.
|
||||
<a
|
||||
href="#"
|
||||
className={'ml-2'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.gridApi.setFilterModel(null);
|
||||
props.gridApi.onFilterChanged();
|
||||
props.setIsFiltered(false);
|
||||
props.setIsFilterReset(true);
|
||||
}}
|
||||
>
|
||||
Retirer tous les filtres
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="m-0">
|
||||
Affichage de <strong>{' ' + props.nbItems + ' ' + itemsLabel}</strong>
|
||||
{selectedText}, vue non filtrée.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{props.isLoadingData && (
|
||||
<div>
|
||||
<Spinner className={'ml-2'} animation="border" variant="secondary" size="sm" />
|
||||
<span className="ml-1">
|
||||
<strong>Chargement des données en cours...</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default InformationsFiltres;
|
||||
@@ -0,0 +1,5 @@
|
||||
.InformationsFiltres {
|
||||
.avertissement {
|
||||
color: #a00;
|
||||
}
|
||||
}
|
||||
21
node/frontend-react/src/components/Commun/Login.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Composant permettant d'indiquer qu'une erreur est survenue.
|
||||
*
|
||||
* @component
|
||||
* @returns
|
||||
*/
|
||||
export const Login = () => (
|
||||
<div className={'container-fluid vertical-center'}>
|
||||
<div className="jumbotron">
|
||||
<h1 className="display-4">Bienvenue à Ogure!</h1>
|
||||
<p className="lead">Aucun utilisateur n'est connecté.</p>
|
||||
<hr />
|
||||
|
||||
<a className="btn btn-primary" href="/accounts/login">
|
||||
Cliquez ici pour vous connecter.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
.Synthese {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.Synthese h5 {
|
||||
margin: 1.5em 0 0 0;
|
||||
color: #0b1b7f;
|
||||
font-size: 1.15em;
|
||||
/*
|
||||
border-bottom: 1px solid #a5acda;
|
||||
padding: 0 0 2px 0;
|
||||
*/
|
||||
}
|
||||
|
||||
.InfoGroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.InfoColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.InfoDisplay {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.NotesRendererDisplay,
|
||||
.MarquesRendererDisplay,
|
||||
.AvisRendererDisplay {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.AvisRendererDisplay {
|
||||
align-items: baseline;
|
||||
.avis {
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
}
|
||||
.SyntheseAddMarque {
|
||||
vertical-align: text-bottom;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.statsAvisDecisions {
|
||||
margin: 0.5em 0 0 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.25em 0.5em;
|
||||
border: 1px solid #999;
|
||||
text-align: center !important;
|
||||
vertical-align: middle;
|
||||
|
||||
&.vide {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
tbody {
|
||||
th {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import './Synthese.scss';
|
||||
import { AvisRendererDisplay, InfoDisplay, NotesRendererDisplay, MarquesRendererDisplay } from './syntheseHelpers';
|
||||
import { AppConfigContext } from '../../../stores/AppConfigStore';
|
||||
import { getCompetences, getDepartements, getDomaineProperty, getFiliereProperty } from '../../Commun/listes/helpers/business-data-helpers';
|
||||
|
||||
/**
|
||||
* Composant "Synthèse d'un administré", présentant un résumé des données sur l'administré.
|
||||
*
|
||||
* @component
|
||||
* @param {array} props.administre - L'administré dont on veut afficher les informations
|
||||
* @param {Object} props.context - Le contexte d'affichage de la liste
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <SyntheseAdministre administre={selectedAdministre} context={...} />
|
||||
* )
|
||||
*/
|
||||
const SyntheseAdministre = observer((props) => {
|
||||
const appConfig = useContext(AppConfigContext);
|
||||
|
||||
// States pour les données modifiables directement dans la fiche de synthèse
|
||||
const [marques, setMarques] = useState(props.administre.PAMEnCours_marques || []);
|
||||
const [avis, setAvis] = useState(props.administre.PAMEnCours_avis || '');
|
||||
const [notesPAM, setNotesPAM] = useState(props.administre.decision_notesUtilisateur || '');
|
||||
const [notesUtilisateur, setNotesUtilisateur] = useState(props.administre.PAMEnCours_notesUtilisateur || '');
|
||||
const [decisionLibelle, setDecisionLibelle] = useState(props.administre.decision_decision || '');
|
||||
|
||||
// Set les valeurs de l'administré sélectionné dans les valeurs éditables
|
||||
useEffect(() => {
|
||||
setMarques(props.administre.PAMEnCours_marques || []);
|
||||
setAvis(props.administre.PAMEnCours_avis || '');
|
||||
setNotesPAM(props.administre.decision_notesUtilisateur || '');
|
||||
setNotesUtilisateur(props.administre.PAMEnCours_notesUtilisateur || '');
|
||||
setDecisionLibelle(props.administre.decision_decision || '');
|
||||
}, [props.administre]);
|
||||
|
||||
// Libellés des compétences
|
||||
let libellesCompetences = [];
|
||||
if (props.administre.competences_competences) {
|
||||
const competences = getCompetences(appConfig.donneesReference, props.administre.competences_competences);
|
||||
libellesCompetences = competences.map((c) => (c ? c.libelle || c.id : ''));
|
||||
}
|
||||
|
||||
let libellesDepartementsSouhaites = [];
|
||||
if (props.administre.FORMOB_departements_souhaites) {
|
||||
const departements = getDepartements(appConfig.donneesReference, props.administre.FORMOB_departements_souhaites);
|
||||
libellesDepartementsSouhaites = departements.map((c) => c.code + (c.code != c.libelle ? ' (' + c.libelle + ')' : ''));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'Synthese'}>
|
||||
<h5>Identité</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Grade'} text={props.administre.identite_grade} />
|
||||
<InfoDisplay label={'Matricule'} text={props.administre.identite_matricule} />
|
||||
<InfoDisplay
|
||||
label={'Date de naissance'}
|
||||
text={props.administre.identite_date_naissance + ' (' + props.administre.identite_age + ' ans)'}
|
||||
/>
|
||||
|
||||
<InfoDisplay label={'Ancienneté grade'} text={props.administre.identite_anciennete_grade} />
|
||||
</div>
|
||||
<div className="InfoColumn" style={{alignItems: 'flex-end'}}>
|
||||
<InfoDisplay label={'Nom'} text={props.administre.identite_nom} />
|
||||
<InfoDisplay label={'Prénom'} text={props.administre.identite_prenom} />
|
||||
<InfoDisplay label={'Sexe'} text={props.administre.identite_sexe === 'F' ? 'Féminin' : 'Masculin'} />
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
<h5>EIP actuel</h5>
|
||||
<div className="InfoGroup">
|
||||
<InfoDisplay
|
||||
label={'Domaine'}
|
||||
text={getDomaineProperty(appConfig.donneesReference, props.administre.EIPActuel_domaine, 'code')}
|
||||
/>
|
||||
<InfoDisplay
|
||||
label={'Filière'}
|
||||
text={getFiliereProperty(appConfig.donneesReference, props.administre.EIPActuel_filiere, 'code')}
|
||||
/>
|
||||
<InfoDisplay label={'NF'} text={props.administre.EIPActuel_NF} />
|
||||
</div>
|
||||
*/}
|
||||
<h5>EIP futur</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay
|
||||
label={'Domaine'}
|
||||
text={getDomaineProperty(appConfig.donneesReference, props.administre.EIPFutur_domaine, 'code')}
|
||||
/>
|
||||
<InfoDisplay
|
||||
label={'Filière'}
|
||||
text={getFiliereProperty(appConfig.donneesReference, props.administre.EIPFutur_filiere, 'code')}
|
||||
/>
|
||||
<InfoDisplay label={'NF'} text={props.administre.EIPFutur_NF} />
|
||||
<InfoDisplay label={'Compétences'} text={libellesCompetences.join(' ; ')} />
|
||||
</div>
|
||||
</div>
|
||||
<h5>Fonction</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Fonction'} text={props.administre.fonction} />
|
||||
</div>
|
||||
</div>{' '}
|
||||
<h5>Matrimonial</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Situation'} text={props.administre.matrimonial_situation} />
|
||||
<InfoDisplay label={'Emploi du conjoint'} text={props.administre.matrimonial_emploiConjoint} />
|
||||
<InfoDisplay label={'Sexe du conjoint'} text={props.administre.matrimonial_sexeConjoint} />
|
||||
</div>
|
||||
</div>
|
||||
<h5>Affectation actuelle</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Garnison'} text={props.administre.affectationActuelle_lieu} />
|
||||
<InfoDisplay label={'FE mère'} text={props.administre.affectationActuelle_libelleFeMere} />
|
||||
<InfoDisplay label={'FE fille'} text={props.administre.affectationActuelle_libelle} />
|
||||
</div>
|
||||
</div>
|
||||
<h5>FORMOB</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Code motif'} text={props.administre.FORMOB_motif} />
|
||||
<InfoDisplay label={'Départements souhaités'} text={libellesDepartementsSouhaites.join(' ; ')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<NotesRendererDisplay label={"Remarques de l'intéressé"} notes={props.administre.FORMOB_remarques} editable={false} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<NotesRendererDisplay label={'Avis CdF'} notes={props.administre.FORMOB_avisCdF} editable={false} />
|
||||
</div>
|
||||
</div>
|
||||
<h5>Commentaires à conserver</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<MarquesRendererDisplay label={'Marques'} marques={marques} setMarques={setMarques} context={props.context} />
|
||||
<InfoDisplay label={'Année prévisible de mutation'} text={props.administre.PAMEnCours_anneePrevisibleMutation} />
|
||||
</div>
|
||||
<div className="InfoColumn" style={{alignItems: 'flex-end'}}>
|
||||
<NotesRendererDisplay label={'Notes'} notes={notesUtilisateur} setNotes={setNotesUtilisateur} editable={false} />
|
||||
</div>
|
||||
</div>
|
||||
<h5>PAM en cours</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<AvisRendererDisplay label={'Avis'} avis={avis} context={props.context} />
|
||||
<InfoDisplay label={'CIAT'} text={props.administre.decision_ciat ? 'CIAT' : ''} />
|
||||
</div>
|
||||
<div className="InfoColumn" style={{alignItems: 'flex-end'}}>
|
||||
<NotesRendererDisplay label={'Notes PAM'} notes={notesPAM} setNotes={setNotesPAM} editable={false} />
|
||||
<InfoDisplay label={'PPE / SHM / ITD'} text={props.administre.decision_specifique} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default SyntheseAdministre;
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import './Synthese.scss';
|
||||
import { InfoDisplay, NotesRendererDisplay, MarquesRendererDisplay } from './syntheseHelpers';
|
||||
import AppConfigStore, { AppConfigContext } from '../../../stores/AppConfigStore';
|
||||
import {
|
||||
getCompetences,
|
||||
getDomaineProperty,
|
||||
getFiliereProperty,
|
||||
getNbAdministresAffectables,
|
||||
getNbAdministresAffectes
|
||||
} from '../../Commun/listes/helpers/business-data-helpers';
|
||||
import { getLibelleAvisPoste } from '../../../helpers/i18n-helper';
|
||||
import i18n from '../../../i18n/i18n';
|
||||
|
||||
/**
|
||||
* Composant "Synthèse d'un poste", présentant un résumé des données sur le poste.
|
||||
*
|
||||
* @component
|
||||
* @param {array} props.poste - Le poste dont on veut afficher les informations
|
||||
* @param {Object} props.context - Le contexte d'affichage de la liste
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <SynthesePoste poste={selectedPoste} context={...} />
|
||||
* )
|
||||
*/
|
||||
const SynthesePoste = observer((props) => {
|
||||
/** @type {AppConfigStore} */
|
||||
const appConfig = useContext(AppConfigContext);
|
||||
|
||||
// States pour les données modifiables directement dans la fiche de synthèse
|
||||
const [marques, setMarques] = useState(null);
|
||||
const [notesUtilisateur, setNotesUtilisateur] = useState(null);
|
||||
const [nbAvisDecidables, setNbAvisDecidables] = useState(null);
|
||||
const [nbTotalDecisions, setNbTotalDecisions] = useState(null);
|
||||
const [nbDecisionsParAvis, setNbDecisionsParAvis] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setMarques(props.poste.PAMEnCours_marques || []);
|
||||
|
||||
setNotesUtilisateur(props.poste.PAMEnCours_notesUtilisateur || '');
|
||||
|
||||
setNbAvisDecidables(getNbAdministresAffectables(appConfig.donneesReference, props.poste));
|
||||
|
||||
const totalDecisions = getNbAdministresAffectes(appConfig.donneesReference, props.poste);
|
||||
setNbTotalDecisions(totalDecisions);
|
||||
|
||||
// Détermination du nb de décisions par avis par répartition du nb total de décision : on sert les prioritaires en 1er
|
||||
let dispo = totalDecisions;
|
||||
let dpa = [];
|
||||
for (let ap of appConfig.donneesReference.avisPoste.filter((ap) => ap.inclusAlgo)) {
|
||||
const nbMax = parseInt(props.poste['p_nb_' + ap.code.toLowerCase()], 10) || 0;
|
||||
const nb = Math.min(dispo, nbMax);
|
||||
dpa.push(nb);
|
||||
dispo -= nb;
|
||||
}
|
||||
setNbDecisionsParAvis(dpa);
|
||||
}, [props.poste]);
|
||||
|
||||
// Libellés des compétences
|
||||
let libellesCompetences = '';
|
||||
if (props.poste.poste_competences) {
|
||||
const competences = getCompetences(appConfig.donneesReference, props.poste.poste_competences);
|
||||
libellesCompetences = competences.map((c) => c.libelle || c.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="Synthese">
|
||||
<h5>PAM en cours</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<NotesRendererDisplay label="Notes" notes={notesUtilisateur} setNotes={setNotesUtilisateur} editable={false} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<MarquesRendererDisplay label="Marques" marques={marques} setMarques={setMarques} context={props.context} />
|
||||
<div> </div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <table className="statsAvisDecisions">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="vide"></th>
|
||||
{appConfig.donneesReference.avisPoste
|
||||
.filter((ap) => ap.inclusAlgo)
|
||||
.map(ap => {
|
||||
const code = ap.code;
|
||||
return (
|
||||
<th key={code} className={'avis poste ' + code}>
|
||||
{i18n.avisPoste?.[code]?.libelleCourt ?? getLibelleAvisPoste(code, appConfig.donneesReference) ?? code}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Nb avis</th>
|
||||
{appConfig.donneesReference.avisPoste
|
||||
.filter((ap) => ap.inclusAlgo)
|
||||
.map((ap) => (
|
||||
<td key={ap.code}>{props.poste['p_nb_' + ap.code.toLowerCase()]}</td>
|
||||
))}
|
||||
<td className="total">{nbAvisDecidables}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Nb décisions</th>
|
||||
{nbDecisionsParAvis.map((dpa, index) => (
|
||||
<td key={index}>{dpa}</td>
|
||||
))}
|
||||
<td className="total">{nbTotalDecisions}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table> */}
|
||||
|
||||
<h5>Organisme</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label="Département" text={props.poste.organisme_departement} />
|
||||
<InfoDisplay label="Code FE" text={props.poste.organisme_FE} />
|
||||
<InfoDisplay label="Code FE mère" text={props.poste.organisme_codeFeMere} />
|
||||
<InfoDisplay label="Libellé" text={props.poste.organisme_libelle} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label="Lieu" text={props.poste.organisme_lieu} />
|
||||
<InfoDisplay label="Zone de défense" text={props.poste.organisme_zone_defense} />
|
||||
<InfoDisplay label="Info REO" text={props.poste.organisme_infoReo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Poste</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label={'Numéro de poste'} text={props.poste.poste_id} />
|
||||
<InfoDisplay label="Domaine" text={getDomaineProperty(appConfig.donneesReference, props.poste.poste_domaine, 'code')} />
|
||||
<InfoDisplay label="Filière" text={getFiliereProperty(appConfig.donneesReference, props.poste.poste_filiere, 'code')} />
|
||||
<InfoDisplay label="NF" text={props.poste.poste_NF} />
|
||||
</div>
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label="ETR" text={props.poste.poste_ETR} />
|
||||
<InfoDisplay label="Compétence requise" text={libellesCompetences.join(' ; ')} />
|
||||
<InfoDisplay label={'CIAT'} text={props.poste.poste_CIAT ? 'CIAT' : ''} />
|
||||
<InfoDisplay label={'PPE / SHM / ITD'} text={props.poste.poste_specifique} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Avis</h5>
|
||||
<div className="InfoGroup">
|
||||
<div className="InfoColumn">
|
||||
<InfoDisplay label="Priorité du poste DRHAT" text={props.poste.priorite_drhat} />
|
||||
<InfoDisplay label="Priorité du poste pour la FE" text={props.poste.priorite_fe} />
|
||||
<InfoDisplay label="Direct / Commissionné" text={props.poste.decisions_administre_id_pam} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default SynthesePoste;
|
||||
@@ -0,0 +1,145 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { useState } from 'react';
|
||||
import { ADMINISTRES } from '../../../constantes/constantes';
|
||||
import i18n from '../../../i18n/i18n';
|
||||
import MarquesPopinEditor from '../editors/marques/MarquesPopinEditor';
|
||||
import NotesEditor from '../editors/notes/NotesEditor';
|
||||
import { getRefs } from '../listes/commonAGGridFunctions';
|
||||
import MarquesRenderer from '../renderers/MarquesRenderer';
|
||||
import NotesRenderer from '../renderers/NotesRenderer';
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Librairie de fonctions et composants utilitaires nécessaires à l'affichage de la synthèse d'un poste et d'un administré.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Affiche un champ avec sa valeur.
|
||||
*
|
||||
* @param {string} props.label Libellé
|
||||
* @param {string} props.text Valeur
|
||||
*/
|
||||
export const InfoDisplay = observer((props) => {
|
||||
return (
|
||||
<div className="InfoDisplay">
|
||||
<strong>{props.label} : </strong> {props.text ? props.text : <span className="donneeAbsente">{i18n.global.donnees.vide}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Affiche un avis et son éditeur.
|
||||
*
|
||||
* @param {string} props.label Libellé
|
||||
* @param {string} props.avis Valeur de l'avis
|
||||
* @param {object} props.context Le contexte d'affichage de la liste
|
||||
*/
|
||||
export const AvisRendererDisplay = observer((props) => {
|
||||
const isAvisAdministre = props.context.groupeType === ADMINISTRES;
|
||||
const avis = isAvisAdministre
|
||||
? getRefs(props)?.avisAdministreById?.get(props.avis)
|
||||
: getRefs(props)?.avisPosteById?.get(props.avis);
|
||||
|
||||
//TODO ajouter éditeur pour l'avis ?
|
||||
|
||||
return (
|
||||
<div className="AvisRendererDisplay">
|
||||
<strong>{props.label} :</strong>
|
||||
{avis ? (
|
||||
<span className={'avis ' + (isAvisAdministre ? 'administre ' : 'poste ') + avis.code}>{avis.libelle}</span>
|
||||
) : (
|
||||
<span className="donneeAbsente">{i18n.global.donnees.vide}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Affiche des notes et leur éditeur
|
||||
*
|
||||
* @params {string} props.label
|
||||
* @params {string} props.notes
|
||||
* @params {function} props.setNotes
|
||||
* @params {boolean} props.editable
|
||||
*/
|
||||
export const NotesRendererDisplay = observer((props) => {
|
||||
const [isNotesEditorOpen, setIsNotesEditorOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="NotesRendererDisplay">
|
||||
<strong>{props.label} :</strong>
|
||||
{props.notes ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (props.editable) {
|
||||
setIsNotesEditorOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<NotesRenderer notes={props.notes} isNotInGrid={true} editable={props.editable} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="donneeAbsente">{i18n.global.donnees.vide}</span>
|
||||
)}
|
||||
</div>
|
||||
<NotesEditor
|
||||
notes={props.notes}
|
||||
setNotes={(notes) => {
|
||||
props.setNotes(notes);
|
||||
console.log('TODO AKA : enregistrer les modifications sur les notes à la fermeture de la pop-in');
|
||||
}}
|
||||
isOpen={isNotesEditorOpen}
|
||||
setIsOpen={setIsNotesEditorOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Affiche des marques et leur éditeur
|
||||
*
|
||||
* @params {string} props.label
|
||||
* @params {string} props.marques
|
||||
* @params {function} props.setMarques
|
||||
* @params {boolean} props.editable
|
||||
*/
|
||||
export const MarquesRendererDisplay = observer((props) => {
|
||||
const [isMarquesEditorOpen, setIsMarquesEditorOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="MarquesRendererDisplay">
|
||||
<strong>{props.label} :</strong>
|
||||
<div
|
||||
onClick={() => {
|
||||
//setIsMarquesEditorOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.marques && props.marques.length ? (
|
||||
<MarquesRenderer marques={props.marques} modeFiltre={false} context={props.context} />
|
||||
) : (
|
||||
<span>{i18n.global.donnees.vide}</span>
|
||||
/*<BsPlusSquare size="15px" className="SyntheseAddMarque donneeAbsente" title="Cliquez pour ajouter des marques..." />*/
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MarquesPopinEditor
|
||||
marques={props.marques}
|
||||
isOpen={isMarquesEditorOpen}
|
||||
setIsOpen={setIsMarquesEditorOpen}
|
||||
context={props.context}
|
||||
typeDonnees={props.context.groupeType}
|
||||
setMarques={(marques) => {
|
||||
props.setMarques(marques);
|
||||
console.log('TODO AKA : enregistrer les modifications sur les marques à la fermeture de la pop-in');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
159
node/frontend-react/src/components/Commun/VoletsFractionnes.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { createRef, useEffect, useState, useRef, useReducer } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import SplitPane from 'react-split-pane';
|
||||
import Pane from 'react-split-pane';
|
||||
|
||||
import { debounce, testLocalStorageAvailable } from '../../helpers/misc-helpers';
|
||||
|
||||
import './VoletsFractionnes.scss';
|
||||
|
||||
/**
|
||||
* Composant "Volets fractionnés" permettant de présenter plusieurs composants React côte-à-côte ou les uns sous les autres,
|
||||
* sous forme de volets dont la taille est redimensionnable par l'utilisateur via la souris.
|
||||
*
|
||||
*
|
||||
* @component
|
||||
* @param {array} props.volets - La liste des volets à afficher
|
||||
* @param {string} props.disposition - La disposition des volets "vertical" (volets verticaux présentés côte-à-côte)
|
||||
* ou "horizontal" (volets horizontaux présentés les uns au dessus des autres)
|
||||
* @param {string} props.persistenceId - Identifiant unique des volets (pour persistence de la configuration en localStorage)
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <VoletsFractionnes disposition="horizontal" volets={[<Volet1 />, <Volet2 />]} persistenceId="MesVolets" />
|
||||
* )
|
||||
*/
|
||||
|
||||
const VoletsFractionnes = observer((props) => {
|
||||
const isLocalStorageAvailable = testLocalStorageAvailable();
|
||||
|
||||
const getStorageKey = () => 'VoletsFractionnes_' + props.persistenceId;
|
||||
|
||||
const [initialSizes, setInitialSizes] = useState(props.paneSizes);
|
||||
|
||||
const splitPaneRef = useRef(null);
|
||||
|
||||
//const [paneRefs, setPaneRefs] = React.useState([]);
|
||||
const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
// Initialisation des paneRefs
|
||||
setPaneRefs((paneRefs) =>
|
||||
Array(props.volets.length || 0)
|
||||
.fill()
|
||||
.map((_, i) => paneRefs[i] || createRef())
|
||||
);
|
||||
}, [props.volets.length]);
|
||||
*/
|
||||
|
||||
const updateInitialSizes = () => {
|
||||
console.log('updateInitialSizes');
|
||||
if (isLocalStorageAvailable && props.persistenceId && props.volets.length == props.paneSizes?.length) {
|
||||
const sizesJSON = localStorage.getItem(getStorageKey());
|
||||
if (!sizesJSON) {
|
||||
setInitialSizes(props.paneSizes);
|
||||
return;
|
||||
}
|
||||
|
||||
let tempInitialSizes = JSON.parse(sizesJSON);
|
||||
|
||||
const initialSizesLength = tempInitialSizes.length;
|
||||
if (props.paneSizes && initialSizesLength < props.paneSizes.length) {
|
||||
// S'il y a moins d'éléments que prévu dans initialSizes, on initialise les dimensions manquantes au prorata de props.paneSizes
|
||||
const dispoARepartir = parseFloat(tempInitialSizes[initialSizesLength - 1]);
|
||||
|
||||
let totalARepartir = 0,
|
||||
i;
|
||||
for (i = initialSizesLength - 1; i < props.paneSizes.length; i++) {
|
||||
totalARepartir += parseFloat(props.paneSizes[i]);
|
||||
}
|
||||
const k = dispoARepartir / totalARepartir;
|
||||
tempInitialSizes.splice(-1, 1);
|
||||
for (i = initialSizesLength - 1; i < props.paneSizes.length; i++) {
|
||||
tempInitialSizes.push(parseFloat(props.paneSizes[i]) * k + '%');
|
||||
}
|
||||
} else if (props.paneSizes && initialSizesLength > props.paneSizes.length && props.paneSizes.length >= 2) {
|
||||
// S'il y a plus d'éléments que prévu dans initialSizes, la taille du dernier volet est égale à la somme de tous ceux en trop
|
||||
let totalDispo = 0;
|
||||
for (let i = props.paneSizes.length - 1; i < initialSizesLength; i++) {
|
||||
totalDispo += parseFloat(tempInitialSizes[i]);
|
||||
}
|
||||
tempInitialSizes.length = props.paneSizes.length - 1;
|
||||
tempInitialSizes.push(totalDispo + '%');
|
||||
}
|
||||
setInitialSizes(tempInitialSizes);
|
||||
} else {
|
||||
setInitialSizes(props.paneSizes);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateInitialSizes();
|
||||
}, [props.paneSizes, props.volets]);
|
||||
|
||||
const onChange = debounce((sizes) => {
|
||||
if (!props.persistenceId || !isLocalStorageAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(getStorageKey(), JSON.stringify(sizes));
|
||||
}, 500);
|
||||
|
||||
const onResizerDoubleClick = (event) => {
|
||||
if (props.persistenceId && isLocalStorageAvailable) {
|
||||
// RAZ aux dimensions par défaut
|
||||
localStorage.removeItem(getStorageKey());
|
||||
}
|
||||
|
||||
setInitialSizes(props.paneSizes);
|
||||
|
||||
/*
|
||||
//setSize non implémenté !! :-@
|
||||
// Redimensionnement aux dimensions par défaut
|
||||
if (props.paneSizes) {
|
||||
for (let i = 0; i < props.paneSizes.length; i++) {
|
||||
//paneRefs[i].current.setSize(props.paneSizes[i]);
|
||||
}
|
||||
}
|
||||
*/
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
// Comme la propriété onResizerDoubleClick de <SplitPane> n'est pas câblée, on le fait artificiellement ici
|
||||
useEffect(() => {
|
||||
let splitPaneDomNode = ReactDOM.findDOMNode(splitPaneRef.current);
|
||||
if (splitPaneDomNode) {
|
||||
const resizers = splitPaneDomNode.querySelectorAll("[data-type='Resizer']");
|
||||
if (resizers?.length) {
|
||||
for (let r of resizers) {
|
||||
r.ondblclick = onResizerDoubleClick;
|
||||
r.title = 'Double-cliquez pour réinitialiser le positionnement des volets';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SplitPane
|
||||
ref={splitPaneRef}
|
||||
minSize={100}
|
||||
split={props.disposition}
|
||||
className={'VoletsFractionnes disposition-' + props.disposition}
|
||||
resizerClassName="VoletsFractionnesResizer"
|
||||
resizerStyle={{ border: '1px solid red' }}
|
||||
onChange={onChange}
|
||||
//onResizerDoubleClick={onResizerDoubleClick}
|
||||
>
|
||||
{props.volets.map((volet, i) => (
|
||||
<Pane /*ref={paneRefs[i]}*/ key={'volet' + i} className="volet" minSize={'10%'} initialSize={initialSizes?.[i]}>
|
||||
{[volet]}
|
||||
</Pane>
|
||||
))}
|
||||
</SplitPane>
|
||||
);
|
||||
});
|
||||
|
||||
export default VoletsFractionnes;
|
||||
@@ -0,0 +1,42 @@
|
||||
.VoletsFractionnes {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
&.disposition-horizontale {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.disposition-verticale {
|
||||
flex-direction: column;
|
||||
|
||||
> .volet {
|
||||
border-top: 3px solid #777;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.volet {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
|
||||
> .ogure-table {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-type='Resizer'] {
|
||||
&[data-attribute='vertical'] {
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
&[data-attribute='horizontal'] {
|
||||
height: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { BsCheckCircle, BsExclamationCircle, BsFillLightningFill, BsXCircle } from 'react-icons/all';
|
||||
import { toast } from 'react-toastify';
|
||||
import useSWR from 'swr';
|
||||
import { CALCUL_TIMEOUT_DELAY, STATUTS_CALCUL } from '../../../../constantes/constantes';
|
||||
import AppConfigStore, { AppConfigContext } from '../../../../stores/AppConfigStore';
|
||||
|
||||
/**
|
||||
* Composant "Zone de calcul" affiché en haut à droite de l'interface, permettant de (re)lancer un calcul et d'indiquer l'état du dernier calcul lancé.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
const CalculSelectif = observer(() => {
|
||||
/** @type {AppConfigStore} */
|
||||
const appConfig = useContext(AppConfigContext);
|
||||
|
||||
const administreId = appConfig.selectedAdministresPerimetre.map((a) => a.id);
|
||||
const annee = appConfig.PAMSelectionne;
|
||||
const anneeA1 = appConfig.donneesReference.PAMByStatut.get('PAM A+1').pam_id;
|
||||
annee != anneeA1 && appConfig.setAdministreSelectifPamA(appConfig.selectedAdministresPerimetre.map((a) => a.id_pam));
|
||||
annee == anneeA1 && appConfig.setAdministreSelectifPamA1(appConfig.selectedAdministresPerimetre.map((a) => a.id_pam));
|
||||
|
||||
const posteId = appConfig.selectedPostesPerimetreEtITD.map((p) => p.id);
|
||||
|
||||
annee != anneeA1 && appConfig.setPosteSelectifPamA(appConfig.selectedPostesPerimetreEtITD.map((p) => p.id_pam));
|
||||
annee == anneeA1 && appConfig.setPosteSelectifPamA1(appConfig.selectedPostesPerimetreEtITD.map((p) => p.id_pam));
|
||||
|
||||
const svId = appConfig.idsSousViviersSelectionnees[0];
|
||||
const pamId = appConfig.PAMSelectionne;
|
||||
|
||||
const getKey = () => appConfig.managers.decisions.buildCalculSelectifStatusKey(svId, administreId, posteId, pamId);
|
||||
const { data, error, mutate } = useSWR(getKey, { refreshInterval: 500 });
|
||||
// clé permettant d'identifier un calcul unique => sa date de lancement
|
||||
const [calculKey, setCalculKey] = useState(new Date());
|
||||
|
||||
// Effet permettant d'afficher un toast de fin de calcul (erreur/succès)
|
||||
|
||||
const hasTimeout = () => {
|
||||
if (typeof data != 'undefined') {
|
||||
if (data.statut == STATUTS_CALCUL.EN_COURS) {
|
||||
let calculStartDate = calculKey;
|
||||
if (typeof data.date_debut != 'undefined') calculStartDate = new Date(data.date_debut);
|
||||
|
||||
let timeoutDate = new Date(calculStartDate.getTime() + CALCUL_TIMEOUT_DELAY);
|
||||
// let timeoutDate = new Date(calculStartDate.getTime() + 20000);
|
||||
let now = new Date();
|
||||
return now > timeoutDate;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Lancer les calculs sur le sous-ensemble selectionné par le gestionnaire
|
||||
*/
|
||||
const lancerCalculSelectif = async (e) => {
|
||||
e.preventDefault();
|
||||
const administreId = appConfig.selectedAdministresPerimetre.map((a) => a.id);
|
||||
const posteId = appConfig.selectedPostesPerimetreEtITD.map((p) => p.id);
|
||||
const svId = appConfig.idsSousViviersSelectionnees[0];
|
||||
const pamId = appConfig.PAMSelectionne;
|
||||
|
||||
// statut = STATUTS_CALCUL.EN_COURS
|
||||
await mutate({ statut: STATUTS_CALCUL.EN_COURS },false);
|
||||
const ok = await appConfig.actionsMetierHelper.lanceCalculSelectif(svId, administreId, posteId, pamId);
|
||||
// On met immédiatement à jour le statut pour que l'affichage soit également immédiatement mis à jour
|
||||
appConfig.setStatutCalculAideDecision(ok ? STATUTS_CALCUL.EN_COURS : STATUTS_CALCUL.ERREUR);
|
||||
};
|
||||
/**
|
||||
* Arreter les calculs sur le sous_vivier selectionner du gestionnaire
|
||||
*/
|
||||
const arreterCalculSelectif = async (e) => {
|
||||
e.preventDefault();
|
||||
const administreId = appConfig.selectedAdministresPerimetre.map((a) => a.id);
|
||||
const posteId = appConfig.selectedPostesPerimetreEtITD.map((p) => p.id);
|
||||
const svId= appConfig.idsSousViviersSelectionnees[0];
|
||||
const pamId = appConfig.PAMSelectionne;
|
||||
|
||||
// statut = STATUTS_CALCUL.EN_COURS
|
||||
await mutate({ statut: STATUTS_CALCUL.EN_ATTENTE_ARRET },false);
|
||||
const ok = await appConfig.actionsMetierHelper.arretCalculSelectif(svId, administreId, posteId, pamId);
|
||||
// On met immédiatement à jour le statut pour que l'affichage soit également immédiatement mis à jour
|
||||
appConfig.setStatutCalculAideDecision(ok ? STATUTS_CALCUL.EN_ATTENTE_ARRET : STATUTS_CALCUL.ERREUR);
|
||||
};
|
||||
|
||||
const isCalcul = true ? appConfig.idsSousViviersSelectionnees.length==1 : false;
|
||||
|
||||
if (isCalcul){
|
||||
return (
|
||||
<div className="mr-4">
|
||||
{!data ? (
|
||||
error ? (
|
||||
'Une erreur est survenue'
|
||||
) : (
|
||||
<div className="spinner-border" role="status" />
|
||||
)
|
||||
) : data && data.statut && [STATUTS_CALCUL.AUCUN, STATUTS_CALCUL.TERMINE_DE_FORCE, STATUTS_CALCUL.TERMINE,STATUTS_CALCUL.EN_ATTENTE].includes(data.statut) ? (
|
||||
data.statut == STATUTS_CALCUL.TERMINE ? (
|
||||
<span>
|
||||
<span style={{ color: 'green' }}>
|
||||
<BsCheckCircle /> Calcul terminé!
|
||||
</span>{' '}
|
||||
<a href="#" onClick={lancerCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Relancer les calculs sur ce sous-ensemble
|
||||
</a>
|
||||
</span>
|
||||
): data.statut == STATUTS_CALCUL.EN_ATTENTE ? (
|
||||
<span className="calculsEnCours">
|
||||
<div className="spinner-border" role="status" />
|
||||
<span> Calculs en attente...</span>
|
||||
<a href="#" onClick={arreterCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Arreter les calculs
|
||||
</a>
|
||||
</span>
|
||||
) : data.statut == STATUTS_CALCUL.TERMINE_DE_FORCE ? (
|
||||
<span>
|
||||
<span style={{ color: 'green' }}>
|
||||
{/* <BsCheckCircle /> Calcul arrêté ! */}
|
||||
</span>{' '}
|
||||
<a href="#" onClick={lancerCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Relancer les calculs de nouveau sur ce sous-ensemble
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<a href="#" onClick={lancerCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Lancer les calculs sur ce sous-ensemble
|
||||
</a>
|
||||
)
|
||||
) : data.statut == STATUTS_CALCUL.ERREUR ? (
|
||||
<span>
|
||||
<span style={{ color: 'orange' }}>
|
||||
<BsXCircle /> Pas de matching pour cette sélection
|
||||
</span>{' '}
|
||||
<a href="#" onClick={lancerCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Relancer les calculs sur ce sous-ensemble
|
||||
</a>
|
||||
</span>
|
||||
|
||||
) : data.statut == STATUTS_CALCUL.EN_ATTENTE_ARRET ? (
|
||||
<span className="calculsEnCours">
|
||||
<div className="spinner-border" role="status" />
|
||||
<span> Calcul en arret ...</span>
|
||||
</span>
|
||||
) : hasTimeout() ? (
|
||||
<span>
|
||||
<span style={{ color: 'orange' }}>
|
||||
<BsExclamationCircle /> Le calcul tourne depuis +1h
|
||||
</span>{' '}
|
||||
<a href="#" onClick={lancerCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Relancer les calculs sur le sous-ensemble
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<span className="calculsEnCours">
|
||||
<div className="spinner-border" role="status" />
|
||||
<span> Calculs en cours </span>
|
||||
<span> </span>
|
||||
<progress value={data.statut_pourcentage} max ={100}/>
|
||||
<span>{data.statut_pourcentage}%</span>
|
||||
<a href="#" onClick={arreterCalculSelectif}>
|
||||
<BsFillLightningFill size={22} />
|
||||
Arreter les calculs
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);};
|
||||
});
|
||||
|
||||
export default CalculSelectif;
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
|
||||
import React, {useEffect, useState, useContext} from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal } from 'react-overlays';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import './CalculsPopinEditor.scss';
|
||||
import './CalculSelectif.jsx';
|
||||
import CalculSelectif from './CalculSelectif.jsx';
|
||||
|
||||
|
||||
/**
|
||||
* Composant permettant de wrapper un éditeur de données (ex : marques, notes, compétences, départements, ...) dans une pop-in (modale).
|
||||
*
|
||||
* @abstract
|
||||
* @component
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {function} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @param {function} props.conditionValidation - Fonction éventuelle qui teste si la validation est possible
|
||||
* @param {function} props.onValidate - Callback éventuelle appelée lorsque l'utilisateur clique sur "Valider"
|
||||
* @param {function} props.onCancel - Callback éventuelle appelée lorsque l'utilisateur clique sur "Annuler" ou ferme la pop-in
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
* @param {string} props.message - Message d'introduction éventuel
|
||||
* @param {*} props.children - Contenu de la pop-in
|
||||
**/
|
||||
|
||||
const CalculsPopinEditor = observer((props) => {
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
|
||||
|
||||
// MAJ de l'état d'ouverture de la modale
|
||||
useEffect(() => {
|
||||
setModalIsOpen(props.isOpen);
|
||||
}, [props.isOpen]);
|
||||
|
||||
// Fond d'écran
|
||||
const Backdrop = () => {
|
||||
return (
|
||||
<div
|
||||
className="Backdrop"
|
||||
onClick={() => {
|
||||
closeModal(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ferme la modale et valide ou annule la saisie effectuée.
|
||||
* @param {boolean} validerSaisie - Indique s'il faut valider la saisie à la fermeture de la modale.
|
||||
*/
|
||||
const closeModal = (validerSaisie) => {
|
||||
if (validerSaisie) {
|
||||
// Validation de la saisie
|
||||
if (props.onValidate) {
|
||||
props.onValidate();
|
||||
}
|
||||
} else {
|
||||
// Annulation de la saisie
|
||||
if (props.onCancel) {
|
||||
props.onCancel();
|
||||
}
|
||||
//toast.info('Modification abandonnée.');
|
||||
}
|
||||
|
||||
setModalIsOpen(false);
|
||||
props.setIsOpen(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={'AbstractPopinEditor' + (props.className ? ' ' + props.className : '')}
|
||||
show={modalIsOpen}
|
||||
renderBackdrop={Backdrop}
|
||||
enforceFocus={false}
|
||||
>
|
||||
<div>
|
||||
|
||||
{props.message && <div className="message">{props.message}</div>}
|
||||
|
||||
<div className="editor">{props.children}</div>
|
||||
|
||||
<div className="actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
closeModal(false);
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<CalculSelectif></CalculSelectif>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CalculsPopinEditor;
|
||||
@@ -0,0 +1,41 @@
|
||||
// .CalculsPopinEditor {
|
||||
// position: fixed;
|
||||
// background: #00000050;
|
||||
// width: 100%;
|
||||
// height: 100vh;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// }
|
||||
|
||||
// .CalculsPopin-inner {
|
||||
// position: relative;
|
||||
// width: 70%;
|
||||
// margin: 0 auto;
|
||||
// height: auto;
|
||||
// max-height: 70vh;
|
||||
// margin-top: calc(100vh - 85vh - 20px);
|
||||
// background: #fff;
|
||||
// border-radius: 4px;
|
||||
// padding: 20px;
|
||||
// border: 1px solid #999;
|
||||
// overflow: auto;
|
||||
// }
|
||||
|
||||
// .CalculsPopin-inner .close-btn{
|
||||
// content: 'x';
|
||||
// cursor: pointer;
|
||||
// position: fixed;
|
||||
// right: calc(15% - 30px);
|
||||
// top: calc(100vh - 85vh - 33px);
|
||||
// background: #ededed;
|
||||
// width: 25px;
|
||||
// height: 25px;
|
||||
// border-radius: 50%;
|
||||
// line-height: 20px;
|
||||
// text-align: center;
|
||||
// border: 1px solid #999;
|
||||
// font-size: 20px;
|
||||
// }
|
||||
.CalculsPopinEditor {
|
||||
max-width: 50em;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, {useEffect, useState, useContext} from 'react';
|
||||
import AppConfigStore, {AppConfigContext} from '../../../../stores/AppConfigStore';
|
||||
|
||||
import './CalculsPopinEditor.scss';
|
||||
import './CalculSelectif.jsx';
|
||||
|
||||
|
||||
|
||||
const Message = ()=>{
|
||||
/** @type {AppConfigStore} */
|
||||
const appConfig = useContext(AppConfigContext);
|
||||
const nbAdministrésAvis = appConfig.selectedAdministresPerimetre.map((a) => a.PAMEnCours_avis).length;
|
||||
const AdministrésAvis = appConfig.selectedAdministresPerimetre.map((a) => a.PAMEnCours_avis);
|
||||
|
||||
|
||||
const nbAdministresSelectionnes = appConfig.selectedAdministresPerimetre.length;
|
||||
const nbPostesSelectionnes = appConfig.selectedPostesPerimetreEtITD.length;
|
||||
const boolNbAdministresNonCalculés = appConfig.selectedAdministresPerimetre.map((a) => a.decision_decision !==undefined);
|
||||
const nbAdministresNonCalculés = appConfig.selectedAdministresPerimetre.map((a) => a.decision_decision).filter(d => d !== undefined).length;
|
||||
let newMessage = <div>
|
||||
<p>{nbAdministresSelectionnes} administrés et {nbPostesSelectionnes} postes ont été sélectionnés.</p>
|
||||
<br></br>
|
||||
<p>Voulez-vous lancer les calculs?</p>
|
||||
</div>
|
||||
|
||||
if (boolNbAdministresNonCalculés.includes(true)){
|
||||
return(
|
||||
<div>
|
||||
<p>{nbAdministresSelectionnes} administrés et {nbPostesSelectionnes} postes ont été sélectionnés.</p>
|
||||
<br></br>
|
||||
<p style={{ color: "red" }}>Attention, la sélection comporte {nbAdministresNonCalculés} administrés avec une décision.
|
||||
<br></br>
|
||||
Ils ne seront donc pas pris en compte par les algorithmes.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else{
|
||||
return(newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal } from 'react-overlays';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import './AbstractPopinEditor.scss';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
/**
|
||||
* Composant permettant de wrapper un éditeur de données (ex : marques, notes, compétences, départements, ...) dans une pop-in (modale).
|
||||
*
|
||||
* @abstract
|
||||
* @component
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {function} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @param {function} props.conditionValidation - Fonction éventuelle qui teste si la validation est possible
|
||||
* @param {function} props.onValidate - Callback éventuelle appelée lorsque l'utilisateur clique sur "Valider"
|
||||
* @param {function} props.onCancel - Callback éventuelle appelée lorsque l'utilisateur clique sur "Annuler" ou ferme la pop-in
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
* @param {string} props.message - Message d'introduction éventuel
|
||||
* @param {*} props.children - Contenu de la pop-in
|
||||
*/
|
||||
const AbstractPopinEditor = observer((props) => {
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
|
||||
// MAJ de l'état d'ouverture de la modale
|
||||
useEffect(() => {
|
||||
setModalIsOpen(props.isOpen);
|
||||
}, [props.isOpen]);
|
||||
|
||||
// Fond d'écran
|
||||
const Backdrop = () => {
|
||||
return (
|
||||
<div
|
||||
className="Backdrop"
|
||||
onClick={() => {
|
||||
closeModal(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ferme la modale et valide ou annule la saisie effectuée.
|
||||
* @param {boolean} validerSaisie - Indique s'il faut valider la saisie à la fermeture de la modale.
|
||||
*/
|
||||
const closeModal = (validerSaisie) => {
|
||||
if (validerSaisie) {
|
||||
// Validation de la saisie
|
||||
if (props.onValidate) {
|
||||
props.onValidate();
|
||||
}
|
||||
} else {
|
||||
// Annulation de la saisie
|
||||
if (props.onCancel) {
|
||||
props.onCancel();
|
||||
}
|
||||
toast.info('Modification abandonnée.');
|
||||
}
|
||||
|
||||
setModalIsOpen(false);
|
||||
props.setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={'AbstractPopinEditor' + (props.className ? ' ' + props.className : '')}
|
||||
show={modalIsOpen}
|
||||
renderBackdrop={Backdrop}
|
||||
enforceFocus={false}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="ExitButton"
|
||||
onClick={() => {
|
||||
closeModal(false);
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
{props.message && <div className="message">{props.message}</div>}
|
||||
|
||||
<div className="editor">{props.children}</div>
|
||||
|
||||
<div className="actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
closeModal(false);
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={props.conditionValidation ? !props.conditionValidation() : false}
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
closeModal(true);
|
||||
}}
|
||||
>
|
||||
Valider
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default AbstractPopinEditor;
|
||||
@@ -0,0 +1,61 @@
|
||||
.AbstractPopinEditor {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1040;
|
||||
border: 1px solid #e5e5e5;
|
||||
background-color: white;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
|
||||
padding: 10px;
|
||||
|
||||
> div {
|
||||
> .ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
> p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
> .message {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
> .editor {
|
||||
margin: 0 -10px;
|
||||
padding: 10px 10px;
|
||||
border-top: 1px solid #bbb;
|
||||
border-bottom: 1px solid #bbb;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
|
||||
import CompetencesEditor from './CompetencesEditor';
|
||||
|
||||
import './CompetencesCellEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de compétences dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
//TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const CompetencesCellEditor = forwardRef((props, ref) => {
|
||||
//console.log('CompetencesCellEditor', props);
|
||||
// Valeur du champ
|
||||
const [competences, setCompetences] = useState(props.value || []);
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return competences;
|
||||
},
|
||||
|
||||
isPopup() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getPopupPosition() {
|
||||
return 'under';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="CompetencesCellEditor"
|
||||
ref={refContainer}
|
||||
tabIndex={1} // important - without this the key presses wont be caught
|
||||
>
|
||||
<div
|
||||
className="ExitButton"
|
||||
onClick={() => {
|
||||
props.api.stopEditing();
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
|
||||
<CompetencesEditor
|
||||
titre="Éditeur de compétences"
|
||||
context={props.context}
|
||||
competences={competences}
|
||||
setCompetences={setCompetences}
|
||||
maxSelectedItems={props.maxSelectedItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default CompetencesCellEditor;
|
||||
@@ -0,0 +1,28 @@
|
||||
.CompetencesCellEditor {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Select from 'react-select';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import './CompetencesEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de compétences.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.competences - Les compétences à afficher/modifier
|
||||
* @param {function} props.setCompetences - Setter pour modifier la valeur des competences
|
||||
* @param {number} props.maxSelectedItems - Nombre maximum de compétences pouvant être sélectionnées
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <CompetencesEditor />
|
||||
* )
|
||||
*/
|
||||
const CompetencesEditor = (props) => {
|
||||
// Liste des compétences possibles
|
||||
const optionsCompetences = getRefs(props)?.competences?.map((c) => ({ value: c.id, label: c.libelle })) ?? [];
|
||||
|
||||
//console.log('CompetenceEditor init', props);
|
||||
|
||||
const [competences, setCompetences] = useState(
|
||||
props.competences
|
||||
? props.competences.flatMap((idComp) => {
|
||||
const oc = optionsCompetences.find((c) => c.value === idComp);
|
||||
if (oc) return oc;
|
||||
|
||||
console.warn('CompetencesEditor - Compétence inconnue : id=' + idComp);
|
||||
return [];
|
||||
})
|
||||
: []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//console.log('CompetenceEditor useEffect', props);
|
||||
|
||||
props.setCompetences(competences.map((c) => c.value));
|
||||
}, [competences]);
|
||||
|
||||
// Nb maximum de compétences sélectionnables
|
||||
const maxSelectedItems = props.maxSelectedItems || 10000000;
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
const refSelect = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
refSelect.current.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const selectStyles = {
|
||||
control: (styles) => ({ ...styles, minWidth: '45em' })
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="CompetencesEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<Select
|
||||
ref={refSelect}
|
||||
isMulti
|
||||
autoFocus
|
||||
options={optionsCompetences}
|
||||
value={competences}
|
||||
isOptionDisabled={(option) => competences.length >= maxSelectedItems}
|
||||
onChange={(newValue, actionMeta) => {
|
||||
setCompetences(newValue);
|
||||
}}
|
||||
className="basic-multi-select"
|
||||
classNamePrefix="select"
|
||||
styles={selectStyles}
|
||||
placeholder="Entrez ici le nom d'une compétence ou sélectionnez-la dans la liste..."
|
||||
noOptionsMessage={(option) => "Aucune compétence correspondante n'a trouvée"}
|
||||
loadingMessage={(option) => 'Chargement en cours...'}
|
||||
/>
|
||||
<div className="actions">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCompetences([]);
|
||||
focus();
|
||||
}}
|
||||
>
|
||||
Effacer toutes les compétences
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetencesEditor;
|
||||
@@ -0,0 +1,12 @@
|
||||
.CompetencesEditor {
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
margin: 0.5em 0 0 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import AbstractPopinEditor from '../common/AbstractPopinEditor';
|
||||
import CompetencesEditor from './CompetencesEditor';
|
||||
|
||||
import './CompetencesPopinEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de de compétences en pop-in.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.competences - Les compétences à afficher/modifier
|
||||
* @param {function} props.setCompetences - Setter pour modifier la valeur des competences
|
||||
* @param {number} props.maxSelectedItems - Nombre maximum de compétences pouvant être sélectionnées
|
||||
* @param {string} props.message - Message d'introduction à afficher
|
||||
*
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {boolean} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @example
|
||||
* return (
|
||||
* <CompetencesEditor />
|
||||
* )
|
||||
*/
|
||||
const CompetencesPopinEditor = observer((props) => {
|
||||
const [competences, setCompetences] = useState(props.competences || []);
|
||||
|
||||
//console.log('CompetencesPopinEditor init', props);
|
||||
|
||||
// MAJ des compétences
|
||||
useEffect(() => {
|
||||
//console.log('CompetencesPopinEditor effect', props);
|
||||
setCompetences(props.competences || []);
|
||||
}, [props.competences]);
|
||||
|
||||
// Validation de la saisie
|
||||
const onValidate = (e) => {
|
||||
// MAJ des compétences si elles ont été modifiées
|
||||
if (JSON.stringify(props.competences) !== JSON.stringify(competences)) {
|
||||
props.setCompetences([...competences]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbstractPopinEditor
|
||||
className="CompetencesPopinEditor"
|
||||
isOpen={props.isOpen}
|
||||
setIsOpen={props.setIsOpen}
|
||||
onValidate={onValidate}
|
||||
message={props.message}
|
||||
titre="Modification groupée de compétences"
|
||||
>
|
||||
<CompetencesEditor
|
||||
titre=""
|
||||
context={props.context}
|
||||
competences={competences}
|
||||
setCompetences={setCompetences}
|
||||
maxSelectedItems={props.maxSelectedItems}
|
||||
/>
|
||||
</AbstractPopinEditor>
|
||||
);
|
||||
});
|
||||
|
||||
export default CompetencesPopinEditor;
|
||||
@@ -0,0 +1,3 @@
|
||||
.CompetencesPopinEditor {
|
||||
max-width: 50em;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { toDayjsAtStartOfDayOrDefault, toDayjsOrNull } from '../../listes/commonAGGridFunctions';
|
||||
|
||||
// format standard (et attendu) pour les inputs de type date
|
||||
const HTML_DATE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
const dateToHtml = (value) => toDayjsOrNull(value)?.format(HTML_DATE_FORMAT) ?? '';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une date dans une cellule d'AG Grid.
|
||||
* fortement inspiré de la documetation : https://www.ag-grid.com/react-data-grid/component-cell-editor/
|
||||
*
|
||||
* @component
|
||||
* @param {any} props.value Valeur initiale (tout type accepté par dayjs, de préférence Date ou dayjs). optionnel
|
||||
* @param {any} props.min Valeur min (tout type accepté par dayjs, de préférence Date ou dayjs). optionnel
|
||||
* @param {any} props.max Valeur max (tout type accepté par dayjs, de préférence Date ou dayjs). optionnel
|
||||
* @param {string} props.className Classe CSS à appliquer. optionnel
|
||||
* @param {boolean} props.nullable Indique si l'absence de valeur est valide. défaut : une valeur vide n'est pas valide
|
||||
*/
|
||||
// TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor) ?
|
||||
const DateCellEditor = forwardRef((props, ref) => {
|
||||
const [value, setValue] = useState(dateToHtml(props.value));
|
||||
const refInput = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// focus on the input
|
||||
setTimeout(() => refInput.current.focus());
|
||||
}, []);
|
||||
|
||||
/* Component Editor Lifecycle methods */
|
||||
useImperativeHandle(ref, () => ({
|
||||
// the final value to send to the grid, on completion of editing
|
||||
getValue: () => {
|
||||
let dateValue = toDayjsAtStartOfDayOrDefault(value, null, HTML_DATE_FORMAT)
|
||||
if (!dateValue) {
|
||||
return dateValue;
|
||||
}
|
||||
// nouvelle valeur invalide, renvoie la valeur initiale
|
||||
if (!dateValue.isValid()) {
|
||||
return props.value;
|
||||
}
|
||||
const min = toDayjsAtStartOfDayOrDefault(props.min);
|
||||
if (min?.isValid() && min.isAfter(dateValue)) {
|
||||
dateValue = min;
|
||||
}
|
||||
const max = toDayjsAtStartOfDayOrDefault(props.max);
|
||||
if (max?.isValid() && max.isBefore(dateValue)) {
|
||||
dateValue = max;
|
||||
}
|
||||
// note : si max avant min il y a de toute façon un problème
|
||||
return dateValue.toDate()
|
||||
},
|
||||
|
||||
// Gets called once when editing is finished (eg if Enter is pressed).
|
||||
// If you return true, then the result of the edit will be ignored.
|
||||
isCancelAfterEnd: () => !props.nullable && !value
|
||||
}));
|
||||
|
||||
const onChange = (event) => setValue(event.target.value);
|
||||
return (
|
||||
<div className="ag-custom-component-popup">
|
||||
<input
|
||||
className={'DateCellEditor ' + (props.className ?? '')}
|
||||
type="date"
|
||||
ref={refInput}
|
||||
value={value}
|
||||
min={dateToHtml(props.min)}
|
||||
max={dateToHtml(props.max)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
DateCellEditor.displayName = 'DateCellEditor';
|
||||
|
||||
export default DateCellEditor;
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
|
||||
import DepartementsEditor from './DepartementsEditor';
|
||||
|
||||
import './DepartementsCellEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de départements dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
*/ //TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const DepartementsCellEditor = forwardRef((props, ref) => {
|
||||
//console.log('DepartementsCellEditor', props);
|
||||
// Valeur du champ
|
||||
const [departements, setDepartements] = useState(props.value || []);
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return departements;
|
||||
},
|
||||
|
||||
isPopup() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getPopupPosition() {
|
||||
return 'under';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="DepartementsCellEditor"
|
||||
ref={refContainer}
|
||||
tabIndex={1} // important - without this the key presses wont be caught
|
||||
>
|
||||
<div
|
||||
className="ExitButton"
|
||||
onClick={() => {
|
||||
props.api.stopEditing();
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
|
||||
<DepartementsEditor
|
||||
titre="Éditeur de départements"
|
||||
context={props.context}
|
||||
departements={departements}
|
||||
setDepartements={setDepartements}
|
||||
maxSelectedItems={props.maxSelectedItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default DepartementsCellEditor;
|
||||
@@ -0,0 +1,28 @@
|
||||
.DepartementsCellEditor {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Select from 'react-select';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import './DepartementsEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de départements.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.departements - Les départements à afficher/modifier
|
||||
* @param {function} props.setDepartements - Setter pour modifier la valeur des departements
|
||||
* @param {number} props.maxSelectedItems - Nombre maximum de départements pouvant être sélectionnés
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <DepartementsEditor />
|
||||
* )
|
||||
*/
|
||||
const DepartementsEditor = (props) => {
|
||||
// Liste des départements possibles
|
||||
const optionsDepartements = getRefs(props)?.departements?.map((d) => ({
|
||||
value: d.id,
|
||||
label: d.code + (d.libelle != d.code ? ' (' + d.libelle + ')' : '')
|
||||
})) ?? [];
|
||||
|
||||
//console.log('DepartementEditor init', props);
|
||||
|
||||
const [departements, setDepartements] = useState(
|
||||
props.departements
|
||||
? props.departements.flatMap((idDept) => {
|
||||
const oc = optionsDepartements.find((d) => d.value === idDept);
|
||||
if (oc) return oc;
|
||||
|
||||
console.warn('DepartementsEditor - Département inconnu : id=' + idDept);
|
||||
return [];
|
||||
})
|
||||
: []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//console.log('DepartementEditor useEffect', props);
|
||||
|
||||
props.setDepartements(departements.map((d) => d.value));
|
||||
}, [departements]);
|
||||
|
||||
// Nb maximum de départements sélectionnables
|
||||
const maxSelectedItems = props.maxSelectedItems || 10000000;
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
const refSelect = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
refSelect.current.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const selectStyles = {
|
||||
control: (styles) => ({ ...styles, minWidth: '45em' })
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="DepartementsEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<Select
|
||||
ref={refSelect}
|
||||
isMulti
|
||||
autoFocus
|
||||
options={optionsDepartements}
|
||||
value={departements}
|
||||
isOptionDisabled={(option) => departements.length >= maxSelectedItems}
|
||||
onChange={(newValue, actionMeta) => {
|
||||
setDepartements(newValue);
|
||||
}}
|
||||
className="basic-multi-select"
|
||||
classNamePrefix="select"
|
||||
styles={selectStyles}
|
||||
placeholder="Entrez ici le code d'un département ou sélectionnez-le dans la liste..."
|
||||
noOptionsMessage={(option) => "Aucun département correspondant n'a trouvé"}
|
||||
loadingMessage={(option) => 'Chargement en cours...'}
|
||||
/>
|
||||
<div className="actions">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDepartements([]);
|
||||
focus();
|
||||
}}
|
||||
>
|
||||
Effacer tous les départements
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartementsEditor;
|
||||
@@ -0,0 +1,12 @@
|
||||
.DepartementsEditor {
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
margin: 0.5em 0 0 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AbstractPopinEditor from '../common/AbstractPopinEditor';
|
||||
import DepartementsEditor from './DepartementsEditor';
|
||||
import './DepartementsPopinEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de départements en pop-in.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.departements - Les départements à afficher/modifier
|
||||
* @param {function} props.setDepartements - Setter pour modifier la valeur des departements
|
||||
* @param {number} props.maxSelectedItems - Nombre maximum de départements pouvant être sélectionnés
|
||||
* @param {string} props.message - Message d'introduction à afficher
|
||||
*
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {boolean} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @example
|
||||
* return (
|
||||
* <DepartementsEditor />
|
||||
* )
|
||||
*/
|
||||
const DepartementsPopinEditor = observer((props) => {
|
||||
const [departements, setDepartements] = useState(props.departements || []);
|
||||
|
||||
//console.log('DepartementsPopinEditor init', props);
|
||||
|
||||
// MAJ des départements
|
||||
useEffect(() => {
|
||||
//console.log('DepartementsPopinEditor effect', props);
|
||||
setDepartements(props.departements || []);
|
||||
}, [props.departements]);
|
||||
|
||||
// Validation de la saisie
|
||||
const onValidate = (e) => {
|
||||
// MAJ des départements si ils ont été modifiés
|
||||
if (JSON.stringify(props.departements) !== JSON.stringify(departements)) {
|
||||
props.setDepartements([...departements]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbstractPopinEditor
|
||||
className="DepartementsPopinEditor"
|
||||
isOpen={props.isOpen}
|
||||
setIsOpen={props.setIsOpen}
|
||||
onValidate={onValidate}
|
||||
message={props.message}
|
||||
titre="Modification groupée de départements"
|
||||
>
|
||||
<DepartementsEditor
|
||||
titre=""
|
||||
context={props.context}
|
||||
departements={departements}
|
||||
setDepartements={setDepartements}
|
||||
maxSelectedItems={props.maxSelectedItems}
|
||||
/>
|
||||
</AbstractPopinEditor>
|
||||
);
|
||||
});
|
||||
|
||||
export default DepartementsPopinEditor;
|
||||
@@ -0,0 +1,3 @@
|
||||
.DepartementsPopinEditor {
|
||||
max-width: 50em;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import './EipEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un EIP.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {object} props.eip - L'EIP à éditer, sous forme d'objet de type {domaineId: ..., filiereId: ..., nfId: ...}
|
||||
* @param {function} props.setEip - Setter pour modifier la valeur de l'EIP
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <EipEditor />
|
||||
* )
|
||||
*/
|
||||
const EipEditor = (props) => {
|
||||
const [eip, setEip] = useState(props.eip || { domaineId: '', filiereId: '', nfId: '' });
|
||||
|
||||
useEffect(() => {
|
||||
props.setEip({
|
||||
domaineId: eip.domaineId || null,
|
||||
filiereId: eip.filiereId || null,
|
||||
nfId: eip.nfId || null
|
||||
});
|
||||
}, [eip]);
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
const refSelectDomaine = useRef(null);
|
||||
|
||||
// Id uniques pour les champs
|
||||
const inputDomaineId = nanoid();
|
||||
const inputFiliereId = nanoid();
|
||||
const inputNfId = nanoid();
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
refSelectDomaine.current.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const refs = getRefs(props);
|
||||
return (
|
||||
<div className="EipEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<form>
|
||||
<div className="field">
|
||||
<label htmlFor={inputDomaineId}>Domaine</label>
|
||||
<span>
|
||||
<select
|
||||
className="domaine"
|
||||
ref={refSelectDomaine}
|
||||
id={inputDomaineId}
|
||||
value={eip.domaineId || ''}
|
||||
onChange={(event) => {
|
||||
setEip({ ...eip, domaineId: event.target.value || '', filiereId: '' });
|
||||
}}
|
||||
>
|
||||
<option>{eip.domaineId}</option>
|
||||
{refs?.domaines?.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.code + ' (' + d.libelle + ')'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor={inputFiliereId}>Filière</label>
|
||||
<span>
|
||||
<select
|
||||
className="filiere"
|
||||
id={inputFiliereId}
|
||||
value={eip.filiereId || ''}
|
||||
onChange={(event) => {
|
||||
setEip({ ...eip, filiereId: event.target.value || null });
|
||||
}}
|
||||
disabled={!eip.domaineId || !refs?.filieres?.filter((f) => f.domaineId == eip.domaineId)?.length}
|
||||
>
|
||||
<option>{eip.filiereId}</option>
|
||||
{eip.domaineId &&
|
||||
refs?.filieres
|
||||
?.filter((f) => f.domaineId == eip.domaineId)
|
||||
?.map((f) => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.code + ' (' + f.libelle + ')'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor={inputNfId}>Niveau fonctionnel</label>
|
||||
<span>
|
||||
<select
|
||||
className="nf"
|
||||
id={inputNfId}
|
||||
value={eip.nfId || ''}
|
||||
onChange={(event) => {
|
||||
setEip({ ...eip, nfId: event.target.value || null });
|
||||
}}
|
||||
>
|
||||
<option>{eip.nfId}</option>
|
||||
{refs?.NF?.map((nf) => (
|
||||
<option key={nf.id} value={nf.id}>
|
||||
{nf.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EipEditor;
|
||||
@@ -0,0 +1,35 @@
|
||||
.EipEditor {
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
form {
|
||||
display: table;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> .field {
|
||||
display: table-row;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
padding: 0.5em 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> label {
|
||||
font-weight: bold;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
> span {
|
||||
> select.domaine,
|
||||
> select.filiere {
|
||||
min-width: 30em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import AbstractPopinEditor from '../common/AbstractPopinEditor';
|
||||
import EipEditor from './EipEditor';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un EIP en pop-in.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.eip - L'EIP à éditer, sous forme d'objet de type {domaineId: ..., filiereId: ..., nfId: ...}
|
||||
* @param {function} props.setEip - Setter pour modifier la valeur de l'EIP
|
||||
* @param {string} props.message - Message d'introduction à afficher
|
||||
*
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {boolean} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @example
|
||||
* return (
|
||||
* <EipEditor />
|
||||
* )
|
||||
*/
|
||||
const EipPopinEditor = observer((props) => {
|
||||
const [eip, setEip] = useState(props.eip || {});
|
||||
|
||||
// MAJ de l'EIP
|
||||
useEffect(() => {
|
||||
// console.log('EipPopinEditor props.eip update', props);
|
||||
setEip(props.eip || {});
|
||||
}, [props.eip]);
|
||||
|
||||
// Callback de validation (= prise en compte) de la saisie
|
||||
const validationSaisie = () => {
|
||||
// MAJ de l'EIP uniquement s'il a été modifié
|
||||
if (JSON.stringify(props.eip) !== JSON.stringify(eip)) {
|
||||
props.setEip({ ...eip });
|
||||
}
|
||||
};
|
||||
|
||||
// Callback de vérification de validité de l'EIP saisi
|
||||
const isEipValid = () => {
|
||||
return (
|
||||
eip && // l'EIP existe
|
||||
eip.domaineId && // Il y a un domaine
|
||||
(eip.filiereId || !getRefs(props)?.filieres?.filter((f) => f.domaineId == eip.domaineId)?.length) && // Il y a une filière sauf si le domaine n'en contient pas
|
||||
eip.nfId // Il y a un NF
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AbstractPopinEditor
|
||||
titre={props.titre}
|
||||
eip={props.eip}
|
||||
className="EipPopinEditor"
|
||||
isOpen={props.isOpen}
|
||||
setIsOpen={props.setIsOpen}
|
||||
onValidate={validationSaisie}
|
||||
conditionValidation={isEipValid}
|
||||
message={props.message}
|
||||
>
|
||||
<EipEditor titre="" context={props.context} eip={eip} setEip={setEip} />
|
||||
</AbstractPopinEditor>
|
||||
);
|
||||
});
|
||||
|
||||
export default EipPopinEditor;
|
||||
@@ -0,0 +1,2 @@
|
||||
.EipPopinEditor {
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
|
||||
import './MarquesCellEditor.scss';
|
||||
|
||||
import MarquesEditor from './MarquesEditor';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un ensemble de marques dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
//TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const MarquesCellEditor = forwardRef((props, ref) => {
|
||||
const [marques, setMarques] = useState(props.value || []);
|
||||
const refContainer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return marques;
|
||||
},
|
||||
|
||||
isPopup() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getPopupPosition() {
|
||||
return 'under';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="MarquesCellEditor"
|
||||
ref={refContainer}
|
||||
tabIndex={1} // important - without this the key presses wont be caught
|
||||
>
|
||||
<div
|
||||
className="ExitButton"
|
||||
onClick={() => {
|
||||
props.api.stopEditing();
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
<MarquesEditor
|
||||
context={props.context}
|
||||
typeDonnees={props.context.groupeType}
|
||||
marques={marques}
|
||||
setMarques={setMarques}
|
||||
titre="Éditeur de marques"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default MarquesCellEditor;
|
||||
@@ -0,0 +1,28 @@
|
||||
.MarquesCellEditor {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ReactSVG } from 'react-svg';
|
||||
import { ADMINISTRES, AFFECTATIONS, POSTES } from '../../../../constantes/constantes';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import { createMarquesComparator } from '../../listes/helpers/business-data-helpers';
|
||||
import './MarquesEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un ensemble de marques.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.typeDonnees - Type de données pour les marques (ADMINISTRES, POSTES ou AFFECTATIONS)
|
||||
* @param {string} props.marques - Les marques à afficher/modifier
|
||||
* @param {void} props.setMarques - Setter pour modifier la valeur des marques
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
* @example
|
||||
* return (
|
||||
* <MarquesEditor />
|
||||
* )
|
||||
*/
|
||||
const MarquesEditor = (props) => {
|
||||
const [marques, setMarques] = useState(props.marques || []);
|
||||
|
||||
//console.log('MarquesEditor init', props.marques, marques);
|
||||
|
||||
useEffect(() => {
|
||||
//console.log('MarquesEditor useEffect', props.marques, marques);
|
||||
props.setMarques(marques || []);
|
||||
}, [marques]);
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const refs = getRefs(props);
|
||||
|
||||
// Liste des groupes concernés à afficher
|
||||
const groupesMarques = refs?.groupesMarques?.filter((groupeMarques) => {
|
||||
if (groupeMarques.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (props.typeDonnees) {
|
||||
case ADMINISTRES:
|
||||
return groupeMarques.type === 'C' || groupeMarques.type === 'M';
|
||||
case POSTES:
|
||||
return groupeMarques.type === 'P';
|
||||
case AFFECTATIONS:
|
||||
return groupeMarques.type === 'C' || groupeMarques.type === 'A';
|
||||
default:
|
||||
console.warn('Type de données inconnu : ' + props.typeDonnees);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
Ajoute ou retire une marque de la liste des marques. S'il existe déjà une marque de même groupe dans la liste, celle-ci est retirée.
|
||||
@param {Object} marqueSel - la marque à ajouter/retirer de la liste
|
||||
*/
|
||||
const toggleMarque = (marqueSel) => {
|
||||
const ajouterMarque = !marques.includes(marqueSel.id);
|
||||
let marquesCopie = [...marques];
|
||||
|
||||
// On retire toutes les marques du même groupe si le groupe n'est pas en sélection multiple
|
||||
if (!refs?.groupesMarquesById.get(marqueSel.groupeMarquesId).selectionMultiple) {
|
||||
for (const marque of refs?.groupesMarquesById.get(marqueSel.groupeMarquesId).marques) {
|
||||
const index = marquesCopie.indexOf(marque.id);
|
||||
if (index !== -1) {
|
||||
marquesCopie.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} else if (refs?.groupesMarquesById.get(marqueSel.groupeMarquesId).selectionMultiple && !ajouterMarque) {
|
||||
const index = marquesCopie.indexOf(marqueSel.id);
|
||||
if (index !== -1) {
|
||||
marquesCopie.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// S'il faut ajouter la marque sélectionnée, on l'ajoute
|
||||
if (ajouterMarque) {
|
||||
marquesCopie.push(marqueSel.id);
|
||||
}
|
||||
|
||||
// On trie la liste des marques par ordre des groupes, puis par marque au sein d'un groupe
|
||||
marquesCopie.sort(createMarquesComparator(getRefs(props)));
|
||||
|
||||
// Mise à jour des marques
|
||||
setMarques(marquesCopie);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="MarquesEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<ul>
|
||||
{groupesMarques.map((groupe) => {
|
||||
if (!groupe.marques || !groupe.marques.length) return;
|
||||
|
||||
return (
|
||||
<li key={groupe.id}>
|
||||
<div className="titre">{groupe.libelle}</div>
|
||||
<ul>
|
||||
{groupe.marques.map((marque) => {
|
||||
const code = groupe.code + '_' + marque.code;
|
||||
const selected = marques.includes(marque.id);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={marque.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleMarque(marque);
|
||||
}}
|
||||
>
|
||||
<ReactSVG
|
||||
src={'assets/images/marques/' + code + '.svg'}
|
||||
className={'pictoMarque ' + code + (selected ? ' selected' : '')}
|
||||
wrapper="span"
|
||||
title={marque.libelle}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="actions">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setMarques([]);
|
||||
}}
|
||||
>
|
||||
Effacer toutes les marques
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarquesEditor;
|
||||
@@ -0,0 +1,79 @@
|
||||
.MarquesEditor {
|
||||
ul,
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
font-size: 1em;
|
||||
|
||||
> p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
/* Groupes de marques */
|
||||
> ul > li {
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
> .titre {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Marques d'un groupe */
|
||||
> ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
min-height: 1.5em;
|
||||
|
||||
> li {
|
||||
margin-right: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> .pictoMarque {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
min-width: 2.3em;
|
||||
min-height: 2.3em;
|
||||
|
||||
padding: 0.3em;
|
||||
border: 1px solid transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--ag-row-hover-color, #ecf0f1);
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
background: var(--ag-selected-row-background-color, #b7e4ff);
|
||||
border-color: var(--ag-range-selection-border-color, var(--ag-balham-active-color, #0091ea));
|
||||
}
|
||||
|
||||
> svg
|
||||
{
|
||||
height: 1.5em;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .actions {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MARQUES_SEPARATEUR } from '../../../../constantes/constantes';
|
||||
import AbstractPopinEditor from '../common/AbstractPopinEditor';
|
||||
import MarquesEditor from './MarquesEditor';
|
||||
import './MarquesPopinEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un ensemble de marques en pop-in.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.typeDonnees - Type de données pour les marques (ADMINISTRES, POSTES ou AFFECTATIONS)
|
||||
* @param {string} props.marques - Les marques à afficher/modifier
|
||||
* @param {function} props.setMarques - Setter pour modifier la valeur des marques
|
||||
* @param {number} props.maxSelectedItems - Nombre maximum de marques pouvant être sélectionnées
|
||||
* @param {string} props.message - Message d'introduction à afficher
|
||||
*
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {boolean} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @example
|
||||
* return (
|
||||
* <MarquesPopinEditor />
|
||||
* )
|
||||
*/
|
||||
const MarquesPopinEditor = observer((props) => {
|
||||
const [marques, setMarques] = useState(props.marques || []);
|
||||
|
||||
//console.log('MarquesPopinEditor init', props.marques, marques);
|
||||
|
||||
// MAJ des marques
|
||||
useEffect(() => {
|
||||
//console.log('MarquesPopinEditor useEffect', props.marques, marques);
|
||||
setMarques(props.marques || []);
|
||||
}, [props.marques]);
|
||||
|
||||
// Validation de la saisie
|
||||
const onValidate = (e) => {
|
||||
// MAJ des marques si elles ont été modifiées
|
||||
if ((props.marques || []).join(MARQUES_SEPARATEUR) !== marques.join(MARQUES_SEPARATEUR)) {
|
||||
props.setMarques([...marques]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbstractPopinEditor
|
||||
className="MarquesPopinEditor"
|
||||
isOpen={props.isOpen}
|
||||
setIsOpen={props.setIsOpen}
|
||||
onValidate={onValidate}
|
||||
message={props.message}
|
||||
titre="Modification groupée de marques"
|
||||
>
|
||||
<MarquesEditor titre="" context={props.context} marques={marques} setMarques={setMarques} typeDonnees={props.typeDonnees} />
|
||||
</AbstractPopinEditor>
|
||||
);
|
||||
});
|
||||
|
||||
export default MarquesPopinEditor;
|
||||
@@ -0,0 +1,3 @@
|
||||
.MarquesPopinEditor {
|
||||
max-width: 40em;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import NotesEditor from './NotesEditor';
|
||||
|
||||
import './NotesCellEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une note (texte riche) dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* return (
|
||||
* <NotesCellEditor />
|
||||
* )
|
||||
*/
|
||||
|
||||
//TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const NotesCellEditor = forwardRef((props, ref) => {
|
||||
const [notes, setNotes] = useState(props.value);
|
||||
|
||||
/* Component Editor Lifecycle methods */
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return notes;
|
||||
},
|
||||
|
||||
isCancelBeforeStart() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isCancelAfterEnd() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// On donne au NotesEditor l'api Ag Grid et le setter de la cellule pour pouvoir modifier la valeur et arrêter l'édition
|
||||
return <NotesEditor notes={props.value} setNotes={setNotes} AGGridApi={props.api} isOpen={true} />;
|
||||
});
|
||||
|
||||
export default NotesCellEditor;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import NotesViewer from './NotesViewer';
|
||||
|
||||
import './NotesCellViewer.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une note (texte riche) dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* return (
|
||||
* <NotesCellViewer />
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
//TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const NotesCellViewer = forwardRef((props, ref) => {
|
||||
const [notes, setNotes] = useState(props.value);
|
||||
|
||||
/* Component Editor Lifecycle methods */
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return notes;
|
||||
},
|
||||
|
||||
isCancelBeforeStart() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isCancelAfterEnd() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// On donne au NotesEditor l'api Ag Grid et le setter de la cellule pour pouvoir modifier la valeur et arrêter l'édition
|
||||
return <NotesViewer notes={props.value} setNotes={setNotes} AGGridApi={props.api} isOpen={true} />;
|
||||
});
|
||||
|
||||
export default NotesCellViewer;
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Modal } from 'react-overlays';
|
||||
import { BsExclamationDiamond, RiCloseCircleFill } from 'react-icons/all';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { BANNED_WORDS } from '../../../../constantes/constantes';
|
||||
|
||||
import './NotesEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une note (texte riche).
|
||||
*
|
||||
* @component
|
||||
* @param {string} props.notes - Les notes à afficher/modifier
|
||||
* @param {void} props.setNotes - Setter pour modifier la valeur des notes dans une cellule Ag Grid
|
||||
* @param {object} props.AGGridApi - Api d'Ag Grid pour arrêter l'édition
|
||||
* @param {boolean} props.isOpen - Permet de savoir si l'éditeur est ouvert ou fermé
|
||||
* @param {void} props.setIsOpen - Permet d'ouvrir ou fermer l'éditeur
|
||||
* @example
|
||||
* return (
|
||||
* <NotesEditor />
|
||||
* )
|
||||
*/
|
||||
const NotesEditor = (props) => {
|
||||
const [value, setValue] = useState(props.notes);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(props.isOpen);
|
||||
const refInput = useRef(null);
|
||||
|
||||
useEffect(() => setValue(props.notes ?? ''), [props.notes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.AGGridApi) {
|
||||
setModalIsOpen(props.isOpen);
|
||||
}
|
||||
}, [props.isOpen]);
|
||||
|
||||
/**
|
||||
* Ferme la modale et met à jour la valeur.
|
||||
*/
|
||||
const setValueAndCloseModal = () => {
|
||||
props.setNotes(value);
|
||||
|
||||
if (props.AGGridApi) {
|
||||
setTimeout(() => {
|
||||
props.AGGridApi.stopEditing();
|
||||
});
|
||||
}
|
||||
|
||||
setModalIsOpen(false);
|
||||
if (props.setIsOpen) {
|
||||
props.setIsOpen(false);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Vérifie la conformité des mots entrés en commentaire avec le RGPD
|
||||
*/
|
||||
const checkRGPD = () => {
|
||||
/**
|
||||
* Convertit du texte en html en texte pour corriger les problèmes d'encoding
|
||||
*/
|
||||
const convertToPlain = (html) => {
|
||||
// Création d'un nouvel élément div
|
||||
var tempDivElement = document.createElement("div");
|
||||
|
||||
// Enregistre le contenu html avec la valeur fournie
|
||||
tempDivElement.innerHTML = html;
|
||||
|
||||
// Retrouve la propriété de texte d'un élément
|
||||
return tempDivElement.textContent || tempDivElement.innerText || "";
|
||||
};
|
||||
if (BANNED_WORDS.some(character => convertToPlain(value).includes(character))) {
|
||||
if (confirm('Votre commentaire contient des mots qui ne sont pas conformes au RGPD \n' +
|
||||
'Voulez-vous quand même poursuivre ?')) {
|
||||
return setValueAndCloseModal();
|
||||
};
|
||||
}
|
||||
else {
|
||||
return setValueAndCloseModal();
|
||||
};
|
||||
};
|
||||
|
||||
const Backdrop = () => {
|
||||
return <div className="Backdrop" onClick={checkRGPD} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="NotesEditorModal" show={modalIsOpen} renderBackdrop={Backdrop} enforceFocus={false}>
|
||||
<div>
|
||||
<div className="ExitButton" onClick={checkRGPD}>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
<div className="AlerteMots">
|
||||
<BsExclamationDiamond size="60px" />
|
||||
<span>
|
||||
<strong>Point d’attention :</strong> Afin d'être en adéquation avec le règlement général sur la protection des données (RGPD),
|
||||
veillez à <strong>ne pas renseigner de données à caractère personnel dans les notes</strong> (données santé, orientations
|
||||
politiques, sexuelles, ethnies présumées...). Vous pouvez vous référer à la liste détaillée des informations à ne pas
|
||||
renseigner dans le guide d'utilisation de l'application.
|
||||
</span>
|
||||
</div>
|
||||
<Editor
|
||||
tinymceScriptSrc='/tinymce/js/tinymce/tinymce.min.js'
|
||||
onInit={(evt, editor) => (refInput.current = editor)}
|
||||
initialValue={props.notes}
|
||||
value={value}
|
||||
onEditorChange={(newValue, editor) => setValue(newValue)}
|
||||
init={{
|
||||
height: '70vh',
|
||||
width: '50vw',
|
||||
resize: false,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print preview anchor',
|
||||
'searchreplace visualblocks code',
|
||||
'insertdatetime media table paste code help wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | bold italic underline backcolor forecolor | bullist numlis | removeformat',
|
||||
content_style:
|
||||
"body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size:13px }",
|
||||
setup: (editor) => {
|
||||
editor.on('init', function (e) {
|
||||
editor.execCommand('mceFocus');
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotesEditor;
|
||||
@@ -0,0 +1,68 @@
|
||||
.Backdrop {
|
||||
position: fixed;
|
||||
z-index: 1040;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.NotesEditorModal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1040;
|
||||
border: 1px solid #e5e5e5;
|
||||
background-color: white;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
font-size: 1.2em;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.AlerteMots {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
z-index: 1040;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
color: #9a0000;
|
||||
max-height: 75px;
|
||||
overflow-y: auto;
|
||||
|
||||
> svg {
|
||||
color: #9a0000;
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Modal } from 'react-overlays';
|
||||
import { BsExclamationDiamond, RiCloseCircleFill } from 'react-icons/all';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
|
||||
import './NotesViewer.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une note (texte riche).
|
||||
*
|
||||
* @component
|
||||
* @param {string} props.notes - Les notes à afficher/modifier
|
||||
* @param {void} props.setNotes - Setter pour modifier la valeur des notes dans une cellule Ag Grid
|
||||
* @param {object} props.AGGridApi - Api d'Ag Grid pour arrêter l'édition
|
||||
* @param {boolean} props.isOpen - Permet de savoir si l'éditeur est ouvert ou fermé
|
||||
* @param {void} props.setIsOpen - Permet d'ouvrir ou fermer l'éditeur
|
||||
* @example
|
||||
* return (
|
||||
* <NotesViewer />
|
||||
* )
|
||||
*/
|
||||
const NotesViewer = (props) => {
|
||||
const [value, setValue] = useState(props.notes);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(props.isOpen);
|
||||
const refInput = useRef(null);
|
||||
|
||||
useEffect(() => setValue(props.notes ?? ''), [props.notes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.AGGridApi) {
|
||||
setModalIsOpen(props.isOpen);
|
||||
}
|
||||
}, [props.isOpen]);
|
||||
|
||||
/**
|
||||
* Ferme la modale et met à jour la valeur.
|
||||
*/
|
||||
const setValueAndCloseModal = () => {
|
||||
props.setNotes(value);
|
||||
|
||||
if (props.AGGridApi) {
|
||||
setTimeout(() => {
|
||||
props.AGGridApi.stopEditing();
|
||||
});
|
||||
}
|
||||
|
||||
setModalIsOpen(false);
|
||||
if (props.setIsOpen) {
|
||||
props.setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const Backdrop = () => {
|
||||
return <div className="Backdrop" onClick={setValueAndCloseModal} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="NotesEditorModal" show={modalIsOpen} renderBackdrop={Backdrop} enforceFocus={false}>
|
||||
<div>
|
||||
<div className="ExitButton" onClick={setValueAndCloseModal}>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
<Editor
|
||||
tinymceScriptSrc='/tinymce/js/tinymce/tinymce.min.js'
|
||||
disabled={true}
|
||||
onInit={(evt, editor) => (refInput.current = editor)}
|
||||
initialValue={props.notes}
|
||||
value={value}
|
||||
menubar={false}
|
||||
init={{
|
||||
height: '70vh',
|
||||
width: '50vw',
|
||||
resize: false,
|
||||
menubar: false,
|
||||
content_style:
|
||||
"body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size:13px }",
|
||||
setup: (editor) => {
|
||||
editor.on('init', function (e) {
|
||||
editor.execCommand('mceFocus');
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotesViewer;
|
||||
@@ -0,0 +1,68 @@
|
||||
.Backdrop {
|
||||
position: fixed;
|
||||
z-index: 1040;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.NotesEditorModal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1040;
|
||||
border: 1px solid #e5e5e5;
|
||||
background-color: white;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
font-size: 1.2em;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.AlerteMots {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
z-index: 1040;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
color: #9a0000;
|
||||
max-height: 75px;
|
||||
overflow-y: auto;
|
||||
|
||||
> svg {
|
||||
color: #9a0000;
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import './NumberCellEditor.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Composant d'édition d'un nombre dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
* @param {number} props.value - Valeur initiale
|
||||
* @param {number} props.min - Valeur min
|
||||
* @param {number} props.max - Valeur max
|
||||
* @param {number} props.step - Pas de l'incrément
|
||||
* @param {string} props.className - Classe CSS à appliquer
|
||||
* @param {boolean} props.nullable - Indique si l'absence de valeur est valide. défaut : une valeur vide n'est pas valide
|
||||
* @param {string} props.placeholder - Texte d'exemple
|
||||
*/
|
||||
// TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor) ?
|
||||
const NumberCellEditor = forwardRef((props, ref) => {
|
||||
const [value, setValue] = useState(Number.parseInt(props.value, 10));
|
||||
const refInput = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// focus on the input
|
||||
setTimeout(() => refInput.current.focus());
|
||||
}, []);
|
||||
|
||||
/* Component Editor Lifecycle methods */
|
||||
useImperativeHandle(ref, () => ({
|
||||
// the final value to send to the grid, on completion of editing
|
||||
getValue() {
|
||||
// n'arrive que si props.nullable
|
||||
if ((value ?? '') === '' || Number.isNaN(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let floatValue = parseFloat(value);
|
||||
|
||||
if (typeof props.step != 'undefined' && props.step !== null && Number.isInteger(props.step) && !Number.isInteger(floatValue)) {
|
||||
floatValue = Math.floor(floatValue);
|
||||
}
|
||||
|
||||
if (typeof props.min != 'undefined' && props.min !== null && value < props.min) {
|
||||
floatValue = Math.max(props.min, floatValue);
|
||||
}
|
||||
|
||||
if (typeof props.max != 'undefined' && props.max !== null && value > props.max) {
|
||||
floatValue = Math.min(props.max, floatValue);
|
||||
}
|
||||
|
||||
return isNaN(floatValue) ? props.min || 0 : floatValue;
|
||||
},
|
||||
|
||||
// Gets called once before editing starts, to give editor a chance to
|
||||
// cancel the editing before it even starts.
|
||||
isCancelBeforeStart() {
|
||||
return false;
|
||||
},
|
||||
|
||||
// Gets called once when editing is finished (eg if Enter is pressed).
|
||||
// If you return true, then the result of the edit will be ignored.
|
||||
isCancelAfterEnd: () => !props.nullable && ((value ?? '') === '' || Number.isNaN(value))
|
||||
}));
|
||||
|
||||
return (
|
||||
<input
|
||||
className={'NumberCellEditor ' + (props.className ?? '')}
|
||||
type="number"
|
||||
ref={refInput}
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
min={props.min ?? ''}
|
||||
max={props.max ?? ''}
|
||||
step={props.step ?? ''}
|
||||
placeholder={props.placeholder ?? ''}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
NumberCellEditor.displayName = 'NumberCellEditor';
|
||||
|
||||
export default NumberCellEditor;
|
||||
@@ -0,0 +1,21 @@
|
||||
.NumberCellEditor {
|
||||
box-sizing: border-box;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--ag-input-border-color, #95a5a6);
|
||||
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
|
||||
background-color: var(--ag-background-color, white);
|
||||
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getManagers } from '../../listes/commonAGGridFunctions';
|
||||
import './PonderationsCellEditor.scss';
|
||||
import PonderationsEditor from './PonderationsEditor';
|
||||
|
||||
/**
|
||||
* Composant d'édition de pondérations dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
//TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const PonderationsCellEditor = forwardRef((props, ref) => {
|
||||
const [cellValue, setValue] = useState(props.value || 0);
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
// Liste des libellés des critères
|
||||
const criteres = [];
|
||||
|
||||
// Liste des nom des champs des critères
|
||||
const fields = [];
|
||||
|
||||
// Liste des pondérations
|
||||
let ponderations = [];
|
||||
|
||||
// --- Construction de la liste des définitions de colonnes pour pouvoir y retrouver les critères de pondération
|
||||
let allColDefs = [];
|
||||
for (let colDef of props.api.getColumnDefs()) {
|
||||
if (colDef.children && colDef.children.length) {
|
||||
// C'est un groupe, on ajoute les colonnes du groupe
|
||||
allColDefs.push(...colDef.children);
|
||||
} else {
|
||||
// Ce n'est pas un groupe
|
||||
allColDefs.push(colDef);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Construction de la liste des critères (libellés et champ) et des pondérations
|
||||
for (let colPondereeId of props.colonnesPonderees) {
|
||||
const colDef = allColDefs.find((colDef) => colDef.colId === colPondereeId);
|
||||
if (colDef) {
|
||||
criteres.push(colDef.headerName || colDef.colId);
|
||||
fields.push(colDef.field);
|
||||
ponderations.push(props?.data[colDef.field] || 0);
|
||||
} else {
|
||||
console.warn('PonderationsCellEditor : colonne ' + colPondereeId + ' non trouvée.');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// A la création du composant
|
||||
focus();
|
||||
|
||||
// Cleanup
|
||||
return function cleanup() {
|
||||
// MAJ des valeurs juste avant la suppression du composant
|
||||
//TODO AKA si possible MAJ de la base en 1 seule requête ici plutôt que dans onCellValueChange
|
||||
if (ponderations) {
|
||||
let data = {};
|
||||
for (let i = 0; i < ponderations.length; i++) {
|
||||
props.node.setDataValue(fields[i], ponderations[i]);
|
||||
data[fields[i]] = ponderations[i];
|
||||
if (props.colDef.field === fields[i]) {
|
||||
setValue(ponderations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
getManagers(props)?.postes.update(props?.data.poste_id, data).catch((reason) => {
|
||||
console.error(reason);
|
||||
toast.error('Une erreur est survenue lors de la modification des pondérations. Veuillez contacter un administrateur.', {
|
||||
autoClose: false
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return cellValue;
|
||||
},
|
||||
|
||||
isPopup() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getPopupPosition() {
|
||||
return 'under';
|
||||
},
|
||||
|
||||
isCancelBeforeStart() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isCancelAfterEnd() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closePonderationEditor = () => {
|
||||
props.api.stopEditing();
|
||||
};
|
||||
|
||||
/**
|
||||
* Met à jour l'ensemble des pondérations.
|
||||
*
|
||||
* @param {Array.<number>} nouvellesPonderations
|
||||
*/
|
||||
const setPonderations = (nouvellesPonderations) => {
|
||||
ponderations = [...nouvellesPonderations];
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="PonderationsCellEditor"
|
||||
ref={refContainer}
|
||||
tabIndex={1} // important - without this the key presses wont be caught
|
||||
>
|
||||
<div className="ExitButton" onClick={closePonderationEditor}>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
<PonderationsEditor
|
||||
titre="Éditeur de pondérations"
|
||||
context={props.context}
|
||||
criteres={criteres}
|
||||
ponderations={ponderations}
|
||||
setPonderations={setPonderations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default PonderationsCellEditor;
|
||||
@@ -0,0 +1,25 @@
|
||||
.PonderationsCellEditor {
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import './PonderationsEditor.scss';
|
||||
import * as ColorManipulation from '../../../../helpers/color-manipulation';
|
||||
import { convertitPonderationsRelativesEnAbsolues } from '../../../../helpers/maths';
|
||||
|
||||
/**
|
||||
* Composant d'édition de pondérations.
|
||||
*
|
||||
* @component
|
||||
* @param {Array.<string>} props.criteres - Liste des libellés des critères à pondérer
|
||||
* @param {Array.<number>} props.ponderations - Liste des valeurs de pondération
|
||||
* @param {function} props.setPonderations - Met à jour les pondérations
|
||||
* @param {Object} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <PonderationsEditor criteres={["Domaine","Filière","NF"]} ponderations={[17,12,3]} setPonderations={...} context={...} />
|
||||
* )
|
||||
*/
|
||||
const PonderationsEditor = (props) => {
|
||||
// Poids maximum
|
||||
const POIDS_MAXIMUM = 5;
|
||||
|
||||
// Pondérations relatives (non arrondies)
|
||||
const [ponderationsRelatives, setPonderationsRelatives] = useState([...props.ponderations]);
|
||||
|
||||
// Pondérations absolues (entier entre 0 et POIDS_MAXIMUM)
|
||||
const [ponderationsAbsolues, setPonderationsAbsolues] = useState(convertitPonderationsRelativesEnAbsolues(props.ponderations));
|
||||
|
||||
// Nb de critères
|
||||
const nbCriteres = props.ponderations.length;
|
||||
|
||||
// Mise à jour des pondérations arrondies
|
||||
useEffect(() => {
|
||||
// MAJ des pondérations
|
||||
props.setPonderations(ponderationsRelatives);
|
||||
}, [ponderationsRelatives]);
|
||||
|
||||
/* Modifie une pondération */
|
||||
const changePonderation = (index, value) => {
|
||||
// Mise à jour des pondérations absolues
|
||||
const newPonderationsAbsolues = [...ponderationsAbsolues];
|
||||
newPonderationsAbsolues[index] = parseInt(value, 10);
|
||||
|
||||
// Mise à jour des pondérations relatives
|
||||
let total = newPonderationsAbsolues.reduce((a, b) => a + b, 0);
|
||||
const newPonderationsRelatives = newPonderationsAbsolues.map((pa) => (pa / total) * 100);
|
||||
|
||||
if (total == 0) {
|
||||
// Erreur, on annule la saisie en gardant les anciennes pondérations
|
||||
setPonderationsAbsolues([...ponderationsAbsolues]);
|
||||
} else {
|
||||
setPonderationsAbsolues(newPonderationsAbsolues);
|
||||
setPonderationsRelatives(newPonderationsRelatives);
|
||||
}
|
||||
};
|
||||
|
||||
/* Equirépartit les pondérations */
|
||||
const equirepartir = () => {
|
||||
setPonderationsAbsolues(Array(nbCriteres).fill(1));
|
||||
setPonderationsRelatives(Array(nbCriteres).fill(100 / nbCriteres));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="PonderationsEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<form>
|
||||
<ul>
|
||||
{props.criteres.map((critere, index) => {
|
||||
return (
|
||||
<li key={critere}>
|
||||
<label htmlFor={'pond_' + critere}>{critere}</label>
|
||||
|
||||
<span className="slider">
|
||||
<input
|
||||
id={'pond_' + critere}
|
||||
type="range"
|
||||
value={Math.round(ponderationsAbsolues[index])}
|
||||
min={0}
|
||||
max={POIDS_MAXIMUM}
|
||||
step={1}
|
||||
onChange={(event) => {
|
||||
changePonderation(index, event.target.value);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="pc">
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: ColorManipulation.getColorForPercentage(
|
||||
ponderationsRelatives[index] / 100,
|
||||
1,
|
||||
ColorManipulation.COLOR_GRADIENT_WHITE_GREEN,
|
||||
true
|
||||
)
|
||||
}}
|
||||
>
|
||||
{ponderationsRelatives[index].toFixed(1)}%
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li className="actions">
|
||||
<span />
|
||||
<span className="slider">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
equirepartir();
|
||||
}}
|
||||
>
|
||||
Equirépartir les pondérations
|
||||
</a>
|
||||
</span>
|
||||
<span />
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PonderationsEditor;
|
||||
@@ -0,0 +1,63 @@
|
||||
.PonderationsEditor {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
font-size: 1em;
|
||||
|
||||
ul {
|
||||
display: table;
|
||||
margin: 1em 0 0 0;
|
||||
|
||||
> li {
|
||||
display: table-row;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
padding: 0.5em 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> label {
|
||||
font-weight: bold;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
> span.slider {
|
||||
padding: 0 1em;
|
||||
|
||||
> input {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
> span.pc {
|
||||
padding-left: 1em;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
min-width: 3.5em;
|
||||
text-align: right;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.actions {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import './PonderationsEditor2.scss';
|
||||
import * as ColorManipulation from '../../../../helpers/color-manipulation';
|
||||
import { arronditPonderations } from '../../../../helpers/maths';
|
||||
|
||||
/**
|
||||
* Composant d'édition de pondérations (ancienne version).
|
||||
*
|
||||
* @component
|
||||
* @param {Array.<string>} props.criteres - Liste des libellés des critères à pondérer
|
||||
* @param {Array.<number>} props.ponderations - Liste des valeurs de pondération
|
||||
* @param {function} props.setPonderations - Met à jour les pondérations
|
||||
* @param {Object} props.context - Le contexte d'affichage de la liste
|
||||
* @example
|
||||
* return (
|
||||
* <PonderationsEditor2 criteres={["Domaine","Filière","NF"]} ponderations={[17,12,3]} setPonderations={...} context={...} />
|
||||
* )
|
||||
*/
|
||||
const PonderationsEditor2 = (props) => {
|
||||
// Total des pondérations à atteindre
|
||||
const TOTAL_PONDERATIONS = 100;
|
||||
|
||||
// Précision des pondérations (nb de décimales)
|
||||
const PRECISION = 0;
|
||||
|
||||
// Nb de critères / pondérations
|
||||
const nbCriteres = props.criteres.length;
|
||||
|
||||
// Pondérations flottantes (non arrondies) nécessaires au bon fonctionnement de l'éditeur
|
||||
const [ponderationsFloat, setPonderationsFloat] = useState([...props.ponderations]);
|
||||
|
||||
//const [indexDernierCurseurModifié, setIndexDernierCurseurModifié] = useState(0);
|
||||
let indexDernierCurseurModifie = 0;
|
||||
|
||||
// Pondérations arrondies
|
||||
const ponderationsInitialesArrondies = arronditPonderations(props.ponderations, TOTAL_PONDERATIONS, PRECISION);
|
||||
const [ponderationsArrondies, setPonderationsArrondies] = useState(ponderationsInitialesArrondies);
|
||||
|
||||
// Mise à jour des pondérations arrondies
|
||||
useEffect(() => {
|
||||
const pArrondies = arronditPonderations(ponderationsFloat, TOTAL_PONDERATIONS, PRECISION, indexDernierCurseurModifie + 1);
|
||||
|
||||
// Calcul et MAJ des pondérations arrondies
|
||||
setPonderationsArrondies(pArrondies);
|
||||
|
||||
// MAJ des pondérations
|
||||
props.setPonderations(pArrondies);
|
||||
}, [ponderationsFloat]);
|
||||
|
||||
/* Modifie une pondération */
|
||||
const changePonderation = (index, value) => {
|
||||
indexDernierCurseurModifie = index;
|
||||
const newPonderationsFloat = [...ponderationsFloat];
|
||||
|
||||
let dispo = TOTAL_PONDERATIONS;
|
||||
for (let i = 0; i < index; i++) {
|
||||
dispo -= ponderationsFloat[i];
|
||||
}
|
||||
|
||||
// Bornage de la pondération
|
||||
const newValue = Math.min(value, dispo);
|
||||
|
||||
// Calcul de l'écart
|
||||
const delta = ponderationsFloat[index] - newValue;
|
||||
|
||||
// Mise à jour de la pondération
|
||||
newPonderationsFloat[index] = newValue;
|
||||
|
||||
dispo -= newValue;
|
||||
|
||||
// Répartissage du delta sur les critères suivants, au prorata de leur valeur
|
||||
let reliquatTotalActuel = 0;
|
||||
for (let i = index + 1; i < nbCriteres; i++) {
|
||||
reliquatTotalActuel += ponderationsFloat[i];
|
||||
}
|
||||
|
||||
dispo = Math.max(dispo, 0);
|
||||
|
||||
dispo += Math.pow(10, -2 - PRECISION); // astuce pour conserver les proportions entre les critères restants en cas de remise à (presque) zéro
|
||||
|
||||
if (reliquatTotalActuel > 0) {
|
||||
const k = dispo / reliquatTotalActuel;
|
||||
for (let i = index + 1; i < nbCriteres; i++) {
|
||||
newPonderationsFloat[i] = Math.max(0, ponderationsFloat[i] * k);
|
||||
}
|
||||
} else {
|
||||
// La somme des critères restants à répartir étant nulle, on équirépartit entre ces critères
|
||||
const v = dispo / (nbCriteres - index - 1);
|
||||
for (let i = index + 1; i < nbCriteres; i++) {
|
||||
newPonderationsFloat[i] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// MAJ pondérations
|
||||
setPonderationsFloat(newPonderationsFloat);
|
||||
};
|
||||
|
||||
/* Equirépartit les pondérations */
|
||||
const equirepartir = () => {
|
||||
const newPonderationsFloat = [...ponderationsFloat];
|
||||
newPonderationsFloat.fill(TOTAL_PONDERATIONS / nbCriteres);
|
||||
setPonderationsFloat(newPonderationsFloat);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="PonderationsEditor2">
|
||||
<p className="titre">Éditeur de pondérations</p>
|
||||
|
||||
<form>
|
||||
<ul>
|
||||
{props.criteres.map((critere, index) => {
|
||||
const disabled = index === nbCriteres - 1;
|
||||
return (
|
||||
<li key={critere}>
|
||||
<label htmlFor={'pond_' + critere}>{critere}</label>
|
||||
<span className="pc">
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: ColorManipulation.getColorForPercentage(
|
||||
ponderationsArrondies[index] / 100,
|
||||
1,
|
||||
ColorManipulation.COLOR_GRADIENT_WHITE_GREEN,
|
||||
true
|
||||
)
|
||||
}}
|
||||
>
|
||||
{Math.round(ponderationsArrondies[index])}%
|
||||
</span>
|
||||
</span>
|
||||
<span className="slider">
|
||||
<input
|
||||
id={'pond_' + critere}
|
||||
type="range"
|
||||
value={Math.round(ponderationsFloat[index])}
|
||||
min={0}
|
||||
max={TOTAL_PONDERATIONS}
|
||||
disabled={disabled}
|
||||
title={
|
||||
disabled
|
||||
? "La pondération de ce critère n'est pas modifiable car elle est automatiquement déduite des autres pondérations."
|
||||
: ''
|
||||
}
|
||||
onChange={(event) => {
|
||||
changePonderation(index, event.target.value);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<li className="actions">
|
||||
<span />
|
||||
<span />
|
||||
<span className="slider">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
equirepartir();
|
||||
}}
|
||||
>
|
||||
Equirépartir les pondérations
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PonderationsEditor2;
|
||||
@@ -0,0 +1,61 @@
|
||||
.PonderationsEditor2 {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
font-size: 1em;
|
||||
|
||||
ul {
|
||||
display: table;
|
||||
margin: 1em 0 0 0;
|
||||
|
||||
> li {
|
||||
display: table-row;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
padding: 0.5em 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> label {
|
||||
font-weight: bold;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
> span.pc {
|
||||
> span {
|
||||
display: inline-block;
|
||||
min-width: 3.5em;
|
||||
text-align: right;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
> span.slider {
|
||||
padding-left: 1em;
|
||||
> input {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&.actions {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import AbstractPopinEditor from '../common/AbstractPopinEditor';
|
||||
import PonderationsEditor from './PonderationsEditor';
|
||||
|
||||
import './PonderationsPopinEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition de pondérations en pop-in.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props.context - Le contexte d'affichage de la liste
|
||||
* @param {Array.<string>} props.criteres - Liste des libellés des critères à pondérer
|
||||
* @param {Array.<number>} props.ponderations - Liste des valeurs de pondération
|
||||
* @param {function} props.setPonderations - Met à jour les pondérations
|
||||
* @param {string} props.message - Message d'introduction à afficher
|
||||
*
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {boolean} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @example
|
||||
* return (
|
||||
* <PonderationsPopinEditor />
|
||||
* )
|
||||
*/
|
||||
const PonderationsPopinEditor = observer((props) => {
|
||||
//console.log('PonderationsPopinEditor props', props);
|
||||
|
||||
const [ponderations, setPonderations] = useState(props.ponderations || []);
|
||||
|
||||
//console.log('PonderationsPopinEditor init', props);
|
||||
|
||||
// MAJ des compétences
|
||||
useEffect(() => {
|
||||
//console.log('PonderationsPopinEditor effect', props);
|
||||
setPonderations(props.ponderations || []);
|
||||
}, [props.ponderations]);
|
||||
|
||||
// Validation de la saisie
|
||||
const onValidate = (e) => {
|
||||
// MAJ des compétences si elles ont été modifiées
|
||||
if (JSON.stringify(props.ponderations) !== JSON.stringify(ponderations)) {
|
||||
props.setPonderations([...ponderations]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbstractPopinEditor
|
||||
className="PonderationsPopinEditor"
|
||||
isOpen={props.isOpen}
|
||||
setIsOpen={props.setIsOpen}
|
||||
onValidate={onValidate}
|
||||
message={props.message}
|
||||
titre="Modification groupée de compétences"
|
||||
>
|
||||
<PonderationsEditor
|
||||
titre=""
|
||||
context={props.context}
|
||||
criteres={props.criteres}
|
||||
ponderations={ponderations}
|
||||
setPonderations={setPonderations}
|
||||
/>
|
||||
</AbstractPopinEditor>
|
||||
);
|
||||
});
|
||||
|
||||
export default PonderationsPopinEditor;
|
||||
@@ -0,0 +1,2 @@
|
||||
.PonderationsPopinEditor {
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import './PosteSureffectifEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un poste en sureffectif.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {object} props.infos - Les informations du poste à éditer, sous forme d'objet de type {departement: ..., codeFe: ..., lieu: ..., zoneDeDefense: ..., codeFeMere: ...}
|
||||
* @param {function} props.setInfos - Setter pour modifier les informations du poste
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <PosteSureffectifEditor />
|
||||
* )
|
||||
*/
|
||||
const PosteSureffectifEditor = (props) => {
|
||||
const [infos, setInfos] = useState(props.infos || { departement: '', codeFe: '',lieu: '', zoneDeDefense: '', codeFeMere: '', etr: ''});
|
||||
useEffect(() => {
|
||||
props.setInfos({
|
||||
departement : infos.departement || null,
|
||||
codeFe : infos.codeFe || null,
|
||||
lieu : infos.lieu || null,
|
||||
zoneDeDefense: infos.zoneDeDefense || null,
|
||||
codeFeMere : infos.codeFeMere || null,
|
||||
etr : infos.etr || null,
|
||||
});
|
||||
}, [infos]);
|
||||
|
||||
const refContainer = useRef(null);
|
||||
const refSelectDepartement = useRef(null);
|
||||
|
||||
// Id uniques pour les champs
|
||||
const inputDepatementId = nanoid();
|
||||
const inputCodeFeId = nanoid();
|
||||
const inputLieuId = nanoid();
|
||||
const inputZoneDeDefenseId = nanoid();
|
||||
const inputCodeFeMereId = nanoid();
|
||||
const inputEtrId = nanoid();
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
refSelectDepartement.current.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const refs = getRefs(props);
|
||||
|
||||
const feFilles = refs?.FEs?.map((fe) => {return {'code': fe.code, 'libelle': fe.libelle}});
|
||||
const feMeres = refs?.FEs?.map((fe) => {return {'mere_code': fe.mere_code, 'mere_la': fe.mere_la}});
|
||||
const lieux = refs?.FEs?.map((fe) => fe.fe_garnison_lieu).filter(ele => {return ele != null});
|
||||
const zonesDef = refs?.FEs?.map((fe) => fe.zone_defense).filter(ele => {return ele != null});
|
||||
const setFeFilles = [...new Map(feFilles.map(item => [JSON.stringify(item), item])).values()];
|
||||
const setFeMeres = [...new Map(feMeres.map(item => [JSON.stringify(item), item])).values()];
|
||||
const setLieux = Array.from(new Set(lieux))
|
||||
.sort((a, b) => (a || '').localeCompare(b));
|
||||
const setZonesDef = Array.from(new Set(zonesDef))
|
||||
.sort((a, b) => (a || '').localeCompare(b));
|
||||
|
||||
const setEtr = refs.etr?.map((fon) => {return {'id': fon.id, 'libelle':fon.libelle}})
|
||||
|
||||
const compare = (a,b) => {
|
||||
const libelleA = a.libelle.toUpperCase();
|
||||
const libelleB = b.libelle.toUpperCase();
|
||||
|
||||
let comparison = 0;
|
||||
if (libelleA > libelleB) {
|
||||
comparison = 1;
|
||||
} else if (libelleA < libelleB) {
|
||||
comparison = -1;
|
||||
}
|
||||
return comparison;
|
||||
}
|
||||
|
||||
setEtr.sort(compare);
|
||||
|
||||
return (
|
||||
<div className="PosteSureffectifEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<form>
|
||||
<div className="field">
|
||||
<label htmlFor={inputDepatementId}>Département</label>
|
||||
<span>
|
||||
<select
|
||||
className="infos"
|
||||
ref={refSelectDepartement}
|
||||
id={inputDepatementId}
|
||||
value={infos.departement || ''}
|
||||
onChange={(event) => {
|
||||
setInfos({ ...infos, departement: event.target.value || ''});
|
||||
}}
|
||||
>
|
||||
<option>{infos.departement}</option>
|
||||
{refs?.departements?.map((d) => (
|
||||
<option key={d.id} value={d.code}>
|
||||
{d.code + ' (' + d.libelle + ')'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor={inputCodeFeId}>Code FE</label>
|
||||
<span>
|
||||
<select
|
||||
className="infos"
|
||||
id={inputCodeFeId}
|
||||
value={infos.codeFe || ''}
|
||||
onChange={(event) => {
|
||||
setInfos({ ...infos, codeFe: event.target.value || ''});
|
||||
}}
|
||||
>
|
||||
<option>{infos.codeFe}</option>
|
||||
{setFeFilles?.map((f) => (
|
||||
<option key={f.code} value={f.code}>
|
||||
{f.code + ' (' + f.libelle + ')'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor={inputLieuId}>Lieu</label>
|
||||
<span>
|
||||
<select
|
||||
className="infos"
|
||||
id={inputLieuId}
|
||||
value={infos.lieu || ''}
|
||||
onChange={(event) => {
|
||||
setInfos({ ...infos, lieu: event.target.value || ''});
|
||||
}}
|
||||
>
|
||||
<option>{infos.lieu}</option>
|
||||
{setLieux?.map((l) => (
|
||||
<option key={l} value={l}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor={inputZoneDeDefenseId}>Zone de défense</label>
|
||||
<span>
|
||||
<select
|
||||
className="infos"
|
||||
id={inputZoneDeDefenseId}
|
||||
value={infos.zoneDeDefense || ''}
|
||||
onChange={(event) => {
|
||||
setInfos({ ...infos, zoneDeDefense: event.target.value || ''});
|
||||
}}
|
||||
>
|
||||
<option>{infos.zoneDeDefense}</option>
|
||||
{setZonesDef?.map((z) => (
|
||||
<option key={z} value={z}>
|
||||
{z}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor={inputCodeFeMereId}>Code FE mère</label>
|
||||
<span>
|
||||
<select
|
||||
className="infos"
|
||||
id={inputCodeFeMereId}
|
||||
value={infos.codeFeMere || ''}
|
||||
onChange={(event) => {
|
||||
setInfos({ ...infos, codeFeMere: event.target.value || ''});
|
||||
}}
|
||||
>
|
||||
<option>{infos.codeFeMere}</option>
|
||||
{setFeMeres?.map((f) => (
|
||||
<option key={f.mere_code} value={f.mere_code}>
|
||||
{f.mere_code + ' (' + f.mere_la + ')'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor={inputEtrId}>ETR</label>
|
||||
<span>
|
||||
<select
|
||||
className="infos"
|
||||
id={inputEtrId}
|
||||
value={infos.etr || ''}
|
||||
onChange={(event) => {
|
||||
setInfos({ ...infos, etr: event.target.value || ''});
|
||||
}}
|
||||
>
|
||||
<option>{infos.etr}</option>
|
||||
{setEtr?.map((e) => (
|
||||
<option key={e.id} value={e.libelle}>
|
||||
{e.libelle}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PosteSureffectifEditor;
|
||||
@@ -0,0 +1,35 @@
|
||||
.PosteSureffectifEditor {
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
form {
|
||||
display: table;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> .field {
|
||||
display: table-row;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
padding: 0.5em 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> label {
|
||||
font-weight: bold;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
> span {
|
||||
> select.infos,
|
||||
> select.filiere {
|
||||
min-width: 30em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
import AbstractPopinEditor from '../common/AbstractPopinEditor';
|
||||
import PosteSureffectifEditor from './PosteSureffectifEditor';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'un poste en sureffectif en pop-in.
|
||||
*
|
||||
* @component
|
||||
* @param {import('../../../../jsdoc/types').ContexteAgGrid} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.infos - Les informations du poste à éditer, sous forme d'objet de type {departement: ..., codeFe: ..., lieu: ..., zoneDeDefense: ..., codeFeMere: ...}
|
||||
* @param {function} props.setInfos - Setter pour modifier les informations du poste
|
||||
* @param {string} props.message - Message d'introduction à afficher
|
||||
*
|
||||
* @param {boolean} props.isOpen - Etat d'ouverture de la pop-in
|
||||
* @param {boolean} props.setIsOpen - Mise à jour de l'état d'ouverture de la pop-in
|
||||
* @example
|
||||
* return (
|
||||
* <PosteSureffectifEditor />
|
||||
* )
|
||||
*/
|
||||
const PosteSureffectifPopinEditor = observer((props) => {
|
||||
const [infos, setInfos] = useState(props.infos || {});
|
||||
const [disable, setDisable] = React.useState(false);
|
||||
|
||||
|
||||
// MAJ du poste en sureffectif
|
||||
useEffect(() => {
|
||||
setInfos(props.infos || {});
|
||||
}, [props.infos]);
|
||||
|
||||
const nonValide = () => {
|
||||
return (
|
||||
<button disabled={disable} onClick={() => setDisable(true)}>
|
||||
Click me!
|
||||
</button>)
|
||||
}
|
||||
// Callback de validation (= prise en compte) de la saisie
|
||||
const validationSaisie = () => {
|
||||
// MAJ de l'EIP uniquement s'il a été modifié
|
||||
if (JSON.stringify(props.infos) !== JSON.stringify(infos)) {
|
||||
props.setInfos({ ...infos });
|
||||
}
|
||||
};
|
||||
|
||||
// Callback de vérification de validité de l'EIP saisi
|
||||
const isInfosValid = () => {
|
||||
return (
|
||||
infos &&
|
||||
infos.departement &&
|
||||
infos.codeFe &&
|
||||
infos.lieu &&
|
||||
infos.zoneDeDefense &&
|
||||
infos.codeFeMere &&
|
||||
infos.etr
|
||||
);
|
||||
};
|
||||
return (
|
||||
<AbstractPopinEditor
|
||||
titre={props.titre}
|
||||
className="PosteSureffectifPopinEditor"
|
||||
isOpen={props.isOpen}
|
||||
setIsOpen={props.setIsOpen}
|
||||
onValidate={validationSaisie}
|
||||
disabled={!infos}
|
||||
conditionValidation={isInfosValid}
|
||||
message={props.message}
|
||||
>
|
||||
<PosteSureffectifEditor titre="" context={props.context} infos={infos} setInfos={setInfos} />
|
||||
</AbstractPopinEditor>
|
||||
);
|
||||
});
|
||||
|
||||
export default PosteSureffectifPopinEditor;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { RiCloseCircleFill } from 'react-icons/all';
|
||||
|
||||
import ZonesEditor from './ZonesEditor';
|
||||
|
||||
import './ZonesCellEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de zones dans une cellule d'AG Grid.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
//TODO composant à refactoriser via un AbstractCellEditor (à la manière de AbstractPopinEditor)
|
||||
const ZonesCellEditor = forwardRef((props, ref) => {
|
||||
|
||||
// Valeur du champ
|
||||
const [zones, setZones] = useState(props.value || []);
|
||||
|
||||
const refContainer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getValue() {
|
||||
return zones;
|
||||
},
|
||||
|
||||
isPopup() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getPopupPosition() {
|
||||
return 'under';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ZonesCellEditor"
|
||||
ref={refContainer}
|
||||
tabIndex={1} // important - without this the key presses wont be caught
|
||||
>
|
||||
<div
|
||||
className="ExitButton"
|
||||
onClick={() => {
|
||||
props.api.stopEditing();
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill />
|
||||
<span>Fermer</span>
|
||||
</div>
|
||||
|
||||
<ZonesEditor
|
||||
titre="Éditeur de zones"
|
||||
context={props.context}
|
||||
zones={zones}
|
||||
setZones={setZones}
|
||||
maxSelectedItems={props.maxSelectedItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default ZonesCellEditor;
|
||||
@@ -0,0 +1,28 @@
|
||||
.ZonesCellEditor {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
|
||||
.ExitButton {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 1040;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
|
||||
> svg {
|
||||
font-size: 1.7em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Select from 'react-select';
|
||||
import { getRefs } from '../../listes/commonAGGridFunctions';
|
||||
|
||||
import './ZonesEditor.scss';
|
||||
|
||||
/**
|
||||
* Composant d'édition d'une liste de zones géographiques SHM.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props.context - Le contexte d'affichage de la liste
|
||||
* @param {string} props.zones - Les zones à afficher/modifier
|
||||
* @param {function} props.setZones - Setter pour modifier la valeur des zones
|
||||
* @param {number} props.maxSelectedItems - Nombre maximum de compétences pouvant être sélectionnées
|
||||
* @param {string} props.titre - Titre éventuel
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <ZonesEditor />
|
||||
* )
|
||||
*/
|
||||
const ZonesEditor = (props) => {
|
||||
// Liste des zones possibles
|
||||
//console.log('CompetenceEditor init', props);
|
||||
|
||||
const optionsZones = getRefs(props)?.Zones?.map((d) => ({
|
||||
value: d.id,
|
||||
label: d.libelle
|
||||
})) ?? [];
|
||||
const [zones, setZones] = useState(
|
||||
props.zones
|
||||
? props.zones.flatMap((Zonelib) => {
|
||||
const oc = optionsZones.find((d) => d.label === Zonelib);
|
||||
if (oc) return oc;
|
||||
|
||||
console.warn('ZonesEditor - Zone inconnu : id=' + Zonelib);
|
||||
return [];
|
||||
})
|
||||
: []
|
||||
)
|
||||
useEffect(() => {
|
||||
//console.log('CompetenceEditor useEffect', props);
|
||||
|
||||
props.setZones(zones.map((d) => d.label));
|
||||
}, [zones]);
|
||||
// Nb maximum de zones sélectionnables
|
||||
const maxSelectedItems = props.maxSelectedItems || 10000000;
|
||||
const refContainer = useRef(null);
|
||||
const refSelect = useRef(null);
|
||||
useEffect(() => {
|
||||
focus();
|
||||
}, []);
|
||||
const focus = () => {
|
||||
window.setTimeout(() => {
|
||||
let container = ReactDOM.findDOMNode(refContainer.current);
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
refSelect.current.focus();
|
||||
});
|
||||
};
|
||||
const selectStyles = {
|
||||
control: (styles) => ({ ...styles, minWidth: '45em' })
|
||||
};
|
||||
return (
|
||||
<div className="ZonesEditor">
|
||||
{props.titre && <p className="titre">{props.titre}</p>}
|
||||
|
||||
<Select
|
||||
ref={refSelect}
|
||||
isMulti
|
||||
autoFocus
|
||||
options={optionsZones}
|
||||
value={zones}
|
||||
isOptionDisabled={(option) => zones.length >= maxSelectedItems}
|
||||
onChange={(newValue, actionMeta) => {
|
||||
setZones(newValue);
|
||||
}}
|
||||
className="basic-multi-select"
|
||||
classNamePrefix="select"
|
||||
styles={selectStyles}
|
||||
placeholder="Entrez ici le nom d'une zone géographique ou sélectionnez-la dans la liste..."
|
||||
noOptionsMessage={(option) => "Aucune zones géographiques correspondante n'a trouvée"}
|
||||
loadingMessage={(option) => 'Chargement en cours...'}
|
||||
/>
|
||||
<div className="actions">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setZones([]);
|
||||
focus();
|
||||
}}
|
||||
>
|
||||
Effacer toutes les zones
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZonesEditor;
|
||||
@@ -0,0 +1,12 @@
|
||||
.ZonesEditor {
|
||||
p.titre {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
margin: 0 5em 0.5em 0;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
margin: 0.5em 0 0 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||