diff --git a/api/src/App.js b/api/src/App.js index 9380accb8..eb3ea3b4d 100644 --- a/api/src/App.js +++ b/api/src/App.js @@ -168,11 +168,7 @@ export default class App extends AppBase { //...scriptSha1Generate([`${__dirname}/front/index.html`]), ], 'default-src': ["'none'"], - 'style-src': [ - "'self'", - ...styleSha1Generate([`${__dirname}/front/index.html`]), - 'cdnjs.cloudflare.com', - ], + 'style-src': ["'self'", ...styleSha1Generate([`${__dirname}/front/index.html`]), 'cdnjs.cloudflare.com'], 'worker-src': ['blob:'], 'frame-src': [ 'https://app.videas.fr/', diff --git a/api/src/models/TableHumanresources.js b/api/src/models/TableHumanresources.js index cfa96021f..4f17ce2c4 100644 --- a/api/src/models/TableHumanresources.js +++ b/api/src/models/TableHumanresources.js @@ -305,6 +305,19 @@ export default (sequelizeInstance, Model) => { } else situation.category_id = findCategory.id } + let findHRToDBByMatricule = await Model.findOne({ + where: { + backup_id: backupId, + matricule: list[i].hmatricule, + }, + logging: false, + }) + + if (list[i].hmatricule !== '' && findHRToDBByMatricule) { + importSituation.push(list[i].hmatricule + ' no add by matricule already existing') + continue + } + switch (code) { case 'MHFJS': code = 'MHFJ' @@ -397,7 +410,7 @@ export default (sequelizeInstance, Model) => { // prepare person const options = { first_name: list[i].prenom || '', - last_name: list[i].nom_usage || list[i].nom_marital || '', + last_name: list[i].nom_usage || list[i].nom_marital || list[i].nom || '', matricule: list[i].hmatricule || '', backup_id: backupId, registration_number: list[i].hRegMatricule, diff --git a/api/src/models/TableTj.js b/api/src/models/TableTj.js index 0bcf3330a..eff432c19 100644 --- a/api/src/models/TableTj.js +++ b/api/src/models/TableTj.js @@ -1,5 +1,5 @@ import { groupBy, sumBy } from 'lodash' -import { preformatHumanResources } from '../utils/ventilator' +import { listCategories } from '../utils/ventilator' export default (sequelizeInstance, Model) => { Model.getAll = async () => { @@ -15,7 +15,7 @@ export default (sequelizeInstance, Model) => { for (let i = 0; i < list.length; i++) { list[i].users = await Model.models.UserVentilations.getUserVentilationsWithLabel(list[i].label) const getBackupId = await Model.models.HRBackups.findByLabel(list[i].label) - const agents = getBackupId ? preformatHumanResources(await Model.models.HumanResources.getCache(getBackupId)) : [] + const agents = getBackupId ? listCategories(await Model.models.HumanResources.getCache(getBackupId)) : [] const group = groupBy( agents.filter((a) => a.category), 'category.label' diff --git a/api/src/routes-api/RouteCalculator.js b/api/src/routes-api/RouteCalculator.js index 56711fee5..d7326415d 100644 --- a/api/src/routes-api/RouteCalculator.js +++ b/api/src/routes-api/RouteCalculator.js @@ -117,7 +117,7 @@ export default class RouteCalculator extends Route { case 'stocks': { const activites = await this.models.Activities.getByMonth(dateStart, backupId, contentieuxId, false) - if (activites.length) { + if (activites && activites.length) { const acti = activites[0] if (acti.stock !== null) { list.push(acti.stock) diff --git a/api/src/routes-api/RouteContentieuxOptions.js b/api/src/routes-api/RouteContentieuxOptions.js index e8b123901..2c98518ed 100644 --- a/api/src/routes-api/RouteContentieuxOptions.js +++ b/api/src/routes-api/RouteContentieuxOptions.js @@ -10,7 +10,7 @@ export default class RouteContentieuxOptions extends Route { * Constructeur * @param {*} params */ - constructor(params) { + constructor (params) { super({ ...params, model: 'ContentieuxOptions' }) } @@ -26,7 +26,7 @@ export default class RouteContentieuxOptions extends Route { }), accesses: [Access.canVewContentieuxOptions], }) - async getAll(ctx) { + async getAll (ctx) { let { juridictionId, backupId } = this.body(ctx) const backups = await this.model.models.OptionsBackups.getBackup(ctx.state.user.id, juridictionId) backupId = backups.find((b) => b.id === backupId) ? backupId : backups.length ? backups[backups.length - 1].id : null @@ -44,7 +44,7 @@ export default class RouteContentieuxOptions extends Route { path: 'remove-backup/:backupId', accesses: [Access.canVewContentieuxOptions], }) - async removeBackup(ctx) { + async removeBackup (ctx) { const { backupId } = ctx.params await this.model.models.OptionsBackups.removeBackup(backupId) @@ -68,7 +68,7 @@ export default class RouteContentieuxOptions extends Route { }), accesses: [Access.canVewContentieuxOptions], }) - async duplicateBackup(ctx) { + async duplicateBackup (ctx) { const { backupId, backupName, backupStatus, type, juridictionId } = this.body(ctx) if (await this.models.OptionsBackups.haveAccess(backupId, juridictionId, ctx.state.user.id)) { @@ -96,7 +96,7 @@ export default class RouteContentieuxOptions extends Route { }), accesses: [Access.canVewContentieuxOptions], }) - async saveBackup(ctx) { + async saveBackup (ctx) { const { backupId, list, backupName, juridictionId, backupStatus, type } = this.body(ctx) if ( (backupId && (await this.models.OptionsBackups.haveAccess(backupId, juridictionId, ctx.state.user.id))) || @@ -104,7 +104,7 @@ export default class RouteContentieuxOptions extends Route { ) { const newId = await this.model.models.OptionsBackups.saveBackup(ctx.state.user.id, list, backupId, backupName, juridictionId, backupStatus, type) - await this.model.models.HistoriesContentieuxUpdate.addHistory(ctx.state.user.id, newId) + if (newId !== null) await this.model.models.HistoriesContentieuxUpdate.addHistory(ctx.state.user.id, newId) this.sendOk(ctx, newId) } else { @@ -126,7 +126,7 @@ export default class RouteContentieuxOptions extends Route { }), accesses: [Access.canVewContentieuxOptions], }) - async renameBackup(ctx) { + async renameBackup (ctx) { const { backupId, backupName, juridictionId } = this.body(ctx) if (await this.models.OptionsBackups.haveAccess(backupId, juridictionId, ctx.state.user.id)) { @@ -144,7 +144,7 @@ export default class RouteContentieuxOptions extends Route { @Route.Get({ accesses: [Access.isAdmin], }) - async getAllAdmin(ctx) { + async getAllAdmin (ctx) { const list = await this.models.OptionsBackups.adminGetAll() const juridictions = await this.models.HRBackups.findAll({ @@ -170,7 +170,7 @@ export default class RouteContentieuxOptions extends Route { }), accesses: [Access.isAdmin], }) - async updateBackup(ctx) { + async updateBackup (ctx) { const { id, juridictions } = this.body(ctx) await this.model.models.OptionsBackupJuridictions.changeRules(id, juridictions) @@ -185,7 +185,7 @@ export default class RouteContentieuxOptions extends Route { path: 'get-backup-details/:backupId', accesses: [Access.canVewContentieuxOptions], }) - async getBackupDetails(ctx) { + async getBackupDetails (ctx) { const { backupId } = ctx.params if (await this.models.OptionsBackups.haveAccessWithoutJuridiction(backupId, ctx.state.user.id)) { @@ -207,7 +207,7 @@ export default class RouteContentieuxOptions extends Route { }), accesses: [Access.canVewContentieuxOptions], }) - async getLastUpdate(ctx) { + async getLastUpdate (ctx) { const { backupId, juridictionId } = this.body(ctx) if (await this.models.OptionsBackups.haveAccess(backupId, juridictionId, ctx.state.user.id)) { diff --git a/api/src/routes/RouteIndex.js b/api/src/routes/RouteIndex.js index acb88b969..37bd7deeb 100644 --- a/api/src/routes/RouteIndex.js +++ b/api/src/routes/RouteIndex.js @@ -53,7 +53,7 @@ export default class RouteIndex extends Route { //stream.on('error', (streamErr) => res.end(streamErr)) } else { const src = createReadStream(file) - ctx.type = mime.getType(file) + ctx.type = mime.getType(file) || 'text/html' ctx.body = src } } else { diff --git a/api/src/utils/ventilator.js b/api/src/utils/ventilator.js index a2d0c6da5..2697123c8 100644 --- a/api/src/utils/ventilator.js +++ b/api/src/utils/ventilator.js @@ -5,7 +5,6 @@ import { findAllIndisponibilities } from './indisponibilities' import { fixDecimal } from './number' export const preformatHumanResources = (list, dateSelected, referentielList, fonctionsIds) => { - console.log('fonctionsIds', fonctionsIds) return orderBy( list.map((h) => { const indisponibilities = dateSelected ? findAllIndisponibilities(h, dateSelected) : [] @@ -63,3 +62,18 @@ export const preformatHumanResources = (list, dateSelected, referentielList, fon return isFiltered }) } + +export const listCategories = (list) => { + return list.map((h) => { + let currentSituation + const situations = h.situations || [] + if (situations.length) { + currentSituation = situations[0] + } + + return { + ...h, + category: currentSituation && currentSituation.category, + } + }) +} diff --git a/front/src/app/components/date-select/date-select.component.scss b/front/src/app/components/date-select/date-select.component.scss index 7e77fe6b4..009238d9d 100644 --- a/front/src/app/components/date-select/date-select.component.scss +++ b/front/src/app/components/date-select/date-select.component.scss @@ -155,6 +155,13 @@ opacity: 0.7; } + &.bottom-popin { + input { + left: -20px; + top: 100%; + } + } + p.title-select { color: fColor(titleSelect); white-space: nowrap; diff --git a/front/src/app/components/input-button/input-button.component.scss b/front/src/app/components/input-button/input-button.component.scss index d429db07f..2839dabec 100644 --- a/front/src/app/components/input-button/input-button.component.scss +++ b/front/src/app/components/input-button/input-button.component.scss @@ -28,6 +28,23 @@ background-color: fColor(darkModeBgMenu); border: 1px solid fColor(darkModeBgMenu); + &.search-bar { + .input { + .input-container { + input { + &::placeholder { + color: white; + opacity: 1; + } + } + + img { + filter: brightness(0) invert(1); + } + } + } + } + p, mat-icon, input { diff --git a/front/src/app/components/options-backup-panel/options-backup-panel.component.ts b/front/src/app/components/options-backup-panel/options-backup-panel.component.ts index d8699f630..9f2899967 100644 --- a/front/src/app/components/options-backup-panel/options-backup-panel.component.ts +++ b/front/src/app/components/options-backup-panel/options-backup-panel.component.ts @@ -163,9 +163,15 @@ export class OptionsBackupPanelComponent .onSaveDatas(isCopy, this.category) .then((x) => { if (isCopy && x !== null) { - //this.router.navigate(['/temps-moyens']) this.contentieuxOptionsService.optionsIsModify.next(false) } + if ( + isCopy === false && + x !== null && + this.contentieuxOptionsService.openedFromCockpit.getValue().value !== + true + ) + this.router.navigate(['/temps-moyens']) }) if ( this.contentieuxOptionsService.openedFromCockpit.getValue().value === true diff --git a/front/src/app/components/panel-activities/progression-bar/progression-bar.component.html b/front/src/app/components/panel-activities/progression-bar/progression-bar.component.html index 72ab13caa..3969854c4 100644 --- a/front/src/app/components/panel-activities/progression-bar/progression-bar.component.html +++ b/front/src/app/components/panel-activities/progression-bar/progression-bar.component.html @@ -1,5 +1,5 @@
Données brutes
Graphiques
ETPT
EAM
Le cockpit vous permet de visualiser en un coup d’œil quelques indicateurs simples, calculés à partir des données d’effectifs et d’activité renseignées dans A-JUST et, si vous le souhaitez, de les comparer à une autre période ou à un référentiel que vous auriez renseigné.
Des visualisations graphiques vous sont également proposées.
Vous pouvez sélectionner la catégorie d'agents souhaitée, restreindre si besoin les calculs à une ou plusieurs fonctions et exporter ces restitutions en PDF pour les enregistrer et/ou les partager.
", }, { target: '.sub-main-header', title: 'Choisir la période', intro: - 'sur laquelle effectuer les calculs. Certaines des données étant des moyennes, elles seront d’autant plus représentatives que la période sélectionnée sera longue.', - }, - { - target: 'aj-referentiel-calculator:first-child .item.actual', - title: 'Les données renseignées', - intro: - "Vous pouvez visualiser, pour chaque contentieux ou sous-contentieux :Cette section permet de visualiser deux indicateurs simples, sur la période, calculés pour chaque contentieux et sous contentieux, à partir des données renseignées dans A-JUST :
Vous retrouvez également :
Pour chaque contentieux, une représentation visuelle des indicateurs, comprenant le détail des données et leurs évolutions entre le début et la fin de la période.
', + beforeLoad: async (intro: any) => { + const itemToClick = document.querySelector('.switch-tab .analytique') + if (itemToClick) { + // @ts-ignore + itemToClick.click() + await sleep(200) + } + }, }, { - target: 'aj-options-backup-panel', - title: 'Mes temps moyens de comparaison', + target: '.drop-down', + title: 'Comparez votre juridiction', intro: - 'Si vous avez renseigné des temps moyens de référence, il vous suffit de sélectionner le référentiel de votre choix dans ce menu déroulant.', + 'Vous pouvez choisir de mettre en perspective les indicateurs de la période choisie avec ceux d’une autre période ou d’un référentiel de temps afin de visualiser les évolutions ou les taux de couverture et DTES de votre juridiction susceptibles de résulter de temps moyens de comparaison renseignés.
Cliquez ici pour créer ou importer un référentiel de temps moyen dans A-JUST.
', + beforeLoad: async (intro: any) => { + const itemToClick = document.querySelector('button.compare') + if (itemToClick) { + // @ts-ignore + itemToClick.click() + await sleep(200) + + const introTooltip = document.querySelector('.introjs-tooltip') + if (introTooltip) { + // @ts-ignore + introTooltip.style.visibility = 'hidden' + } + setTimeout(() => { + const introTooltip = document.querySelector('.introjs-tooltip') + if (introTooltip) { + introTooltip.classList.add('introjs-bottom-left-aligned') + introTooltip.classList.remove('introjs-floating') + // @ts-ignore + introTooltip.style.left = '0px' + // @ts-ignore + introTooltip.style.top = '170px' + // @ts-ignore + introTooltip.style.marginLeft = '-20px' + // @ts-ignore + introTooltip.style.marginTop = '0' + // @ts-ignore + introTooltip.style.visibility = 'visible' + } + }, 380) + } + }, }, ] /** @@ -261,6 +305,15 @@ export class CalculatorPage * Title created from popup while compare loaded */ createdTitle: null | string = null + /** + * Date minimum selectionnable + */ + minDateSelectable: Date = new Date(2021, 0, 1) + /** + * Detect if last month is loading + */ + checkLastMonthLoading: boolean = false + /** * Constructeur * @param humanResourceService @@ -451,23 +504,22 @@ export class CalculatorPage /** * Demande au serveur quelle est la dernière date des datas */ - onCheckLastMonth() { + async onCheckLastMonth(force = false) { if ( - this.calculatorService.dateStart.getValue() === null && - this.referentiel.length + ((!force && this.calculatorService.dateStart.getValue() === null) || + force) && + !this.checkLastMonthLoading ) { - this.activitiesService.getLastMonthActivities().then((date) => { - if (date === null) { - date = new Date() - } - - date = new Date(date ? date : '') + this.checkLastMonthLoading = true + return this.activitiesService.getLastMonthActivities().then((date) => { + date = new Date(date || null) const max = month(date, 0, 'lastday') this.maxDateSelectionDate = max const min = month(max, -11) this.calculatorService.dateStart.next(min) this.calculatorService.dateStop.next(max) + this.checkLastMonthLoading = false this.onLoadComparaisons() }) } @@ -478,6 +530,12 @@ export class CalculatorPage */ onLoadComparaisons(selectedByLabel: string | null = null) { this.backupSettingsService.list([BACKUP_SETTING_COMPARE]).then((l) => { + // clean list from brokens saves + l = l.filter( + (item) => + item.datas && (item.datas.dateStart || item.datas.referentielId) + ) + let refs = this.referentiels let indexRef = -1 do { @@ -605,10 +663,14 @@ export class CalculatorPage this.isLoading = false this.lastCategorySelected = this.categorySelected - if (this.firstLoading === false) + if ( + this.firstLoading === false && + this.location.path() === '/cockpit' + ) { this.appService.notification( 'Les données du cockpit ont été mis à jour !' ) + } this.firstLoading = false }) .catch(() => { @@ -1092,217 +1154,278 @@ export class CalculatorPage this.optionDateStop, false ) + + let dateEndIsPast = false + if (!this.maxDateSelectionDate) { + await this.onCheckLastMonth(true) + } + + if (this.dateStop && this.maxDateSelectionDate) { + dateEndIsPast = isDateBiggerThan( + this.dateStop, + this.maxDateSelectionDate, + true + ) + } + if ( + !dateEndIsPast && + this.optionDateStop && + this.maxDateSelectionDate + ) { + dateEndIsPast = isDateBiggerThan( + this.optionDateStop, + this.maxDateSelectionDate, + true + ) + } + console.log( + dateEndIsPast, + this.dateStop, + this.maxDateSelectionDate, + isDateBiggerThan( + this.dateStop || new Date(), + this.maxDateSelectionDate || new Date(), + true + ), + this.optionDateStop, + this.maxDateSelectionDate, + isDateBiggerThan( + this.optionDateStop || new Date(), + this.maxDateSelectionDate || new Date(), + true + ) + ) + this.appService.appLoading.next(false) const nextRangeString = `${this.getRealValue( this.optionDateStart )} - ${this.getRealValue(this.optionDateStop)}` this.compareAtString = nextRangeString - const value2DTES: (number | null)[] = (resultCalcul.list || []).map( - (d: CalculatorInterface) => d.realDTESInMonths - ) - const variationsDTES = getVariations(value2DTES, value1DTES) - list.push({ - title: 'DTES', - type: 'verticals-lines', - description: 'de la périodeDTES
de la période
@@ -41,7 +41,7 @@Taux de couverture
moyens sur la période
@@ -76,7 +76,7 @@Stock
sur la période
@@ -103,15 +103,14 @@Entrées
sur la période
@@ -176,7 +175,7 @@Sorties
sur la période
@@ -394,54 +393,56 @@Temps moyen Siège
-par dossier observé
- -Fin de période
+Temps moyen Siège
+par dossier observé
+ +Fin de période
+(calculé sur les 12 mois - précédents)
-- {{ datasFilted[index].magRealTimePerCase === null ? 'N/R' : - decimalToStringDate(datasFilted[index].magRealTimePerCase) }} -
+(calculé sur les 12 mois + précédents)
++ {{ datasFilted[index].magRealTimePerCase === null ? 'N/R' : + decimalToStringDate(datasFilted[index].magRealTimePerCase) }} +
+Temps moyen Greffe
-par dossier observé
- -Fin de période
+Temps moyen Greffe
+par dossier observé
+ +Fin de période
+(calculé sur les 12 mois - précédents)
-- {{ datasFilted[index].fonRealTimePerCase === null ? 'N/R' : - decimalToStringDate(datasFilted[index].fonRealTimePerCase) }} -
+(calculé sur les 12 mois + précédents)
++ {{ datasFilted[index].fonRealTimePerCase === null ? 'N/R' : + decimalToStringDate(datasFilted[index].fonRealTimePerCase) }} +
+Gérez vos effectifs et vos données d’activité
{{doc.title}}
Pas d'indisponiblité en cours
+Pas d'indisponibilité en cours
Cet écran qui se présente comme le ventilateur en mode « nuit », c’est-à-dire simulation, vous permet d’adapter finement l’affectation de vos effectifs, à ressources constantes, sur les différents contentieux, en visualisant instantanément l’impact de votre projet de réorganisation sur les délais de traitement du contentieux que vous voulez renforcer mais aussi sur les autres matières que vous allez de ce fait nécessairement dégarnir.
Toutes vos actions sur cet écran n’ont aucun impact sur la ventilation actuelle de vos agents : ce sont que des projections
', + 'Cet écran qui se présente comme le ventilateur en mode « nuit », c’est-à-dire simulation, vous permet d’adapter finement l’affectation de vos effectifs, à ressources constantes, sur les différents contentieux, en visualisant instantanément l’impact de votre projet de réorganisation sur les délais de traitement du contentieux que vous voulez renforcer mais aussi sur les autres matières que vous allez de ce fait nécessairement dégarnir.
Toutes vos actions sur cet écran n’ont aucun impact sur la ventilation actuelle de vos agents : ce ne sont que des projections.
', }, { target: '#content .container .title', - title: 'Choisissez le calendrier, et la catégorie d’agent', + title: 'Choisissez la date et la catégorie d’agent', intro: - "Afin de déterminer le champ d’application des hypothèses que vous allez jouer. Comme dans le ventilateur, en modifiant la date dans le calendrier, vous affichez la liste des agents présents, selon leur catégorie et les ETPT mobilisés sur les différents contentieux à la date choisie. Vous pouvez aussi choisir de n'afficher que les données d'ETPT relatives à certains contentieux, ou à certaines fonctions.
", + 'afin de déterminer le champ d’application des hypothèses que vous allez jouer. Comme dans le ventilateur, en modifiant la date dans le calendrier, vous affichez la liste des agents présents selon leur catégorie ainsi que la ventilation de leur ETPT sur les différents contentieux à la date choisie. Vous pouvez filtrer par fonction ou contentieux selon vos besoins.
', + }, + { + target: '#content .container .title .search-bar', + title: 'Rechercher', + intro: + 'Vous pouvez également rechercher un agent de façon nominative
', }, { target: '#content .container .indicators', title: 'Vos indicateurs d’impact affichent', intro: - "pour chaque contentieux, la situation à la date choisie, en termes d’ETPT affectés, de taux de couverture et de DTES exprimé en nombre de mois.
Ces indicateurs sont construits en lien avec les données d'activité et d’effectifs présentes dans A-JUST. Pour obtenir une projection précise et fine, il convient de compléter et mettre à jour ces éléments en fonction des informations locales dont vous disposez.
", + "pour chaque contentieux, la situation à la date choisie :
Greffe
Entrées moyennes mensuelles
Sorties mensuelles possibles
Stock
ETPT siège
ETPT greffe
ETPT EAM
Taux de couverture
DTES
Temps moyen / dossier
Entrées mensuelles
Sorties mensuelles
Stock
ETPT siège
ETPT greffe
ETPT EAM
Taux de couverture
DTES
Temps moyen / dossier
Vous pouvez effectuer une simulation sans données pré-alimentées en renseignant les données d’effectifs et d’activité correspondantes. Ce peut être utile notamment pour jouer des scenarii sur des activités qui ne sont pas recensées en tant que telles dans A-JUST comme les activités administratives ou le soutien (gestion des scellés par ex.), ou des contentieux qui ne seraient pas isolés spécialement dans A-JUST.
', + beforeLoad: async (intro: any) => { + const itemToClick = document.querySelector('aj-back-button a') + if (itemToClick) { + // @ts-ignore + itemToClick.click() + await sleep(200) + } + }, }, { target: '.categories-switch', @@ -531,7 +539,7 @@ export class SimulatorPage target: '.date-bar-container', title: 'Configurez votre hypothèse :', intro: - 'Commencez par choisir la catégorie d’effectifs pour laquelle vous souhaitez jouer un scénario. Ensuite, déterminez une date de début et de fin de période, c’est à dire la date future à lesquelles vous souhaitez vous projeter (ex : atteindre un stock de X dossier dans 12 mois).
', + 'Commencez par choisir la catégorie d’effectifs pour laquelle vous souhaitez jouer un scénario. Ensuite, déterminez une date de début et de fin de période, c’est à dire la date future à laquelle vous souhaitez vous projeter (ex : atteindre un stock de X dossier dans 12 mois).
', beforeLoad: async (intro: any) => { if (this.periodSelector) { const now = today() diff --git a/front/src/app/routes/simulator/situation-displayer/situation-displayer.component.html b/front/src/app/routes/simulator/situation-displayer/situation-displayer.component.html index 02332dc03..1ad90fe71 100644 --- a/front/src/app/routes/simulator/situation-displayer/situation-displayer.component.html +++ b/front/src/app/routes/simulator/situation-displayer/situation-displayer.component.html @@ -26,11 +26,10 @@