From f328137c30c3674c6f073aab6cd5c5e844f31a01 Mon Sep 17 00:00:00 2001 From: "sebastien.arod@gmail.com" Date: Wed, 5 Jun 2024 10:27:27 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Revue=20des=20stats=20sur=20+=20draft?= =?UTF-8?q?=20p=C3=A9nale=20et=20civile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/ContexteEntreeDC.ts | 6 + src/data/EvenementFamille.ts | 70 +++++++++ src/data/Famille.ts | 35 +---- src/data/StatutFamille.ts | 10 ++ src/data/TypeEvenement.ts | 30 ++++ src/index.ts | 2 +- .../fetchFamiliesWithEventsFromNotion.ts | 12 +- src/notion/publish/format/formatValue.ts | 2 +- .../publish/format/formatValueWithEvol.ts | 2 +- src/notion/publish/publishPeriodStats.ts | 80 ++++++++-- src/notion/publish/publishStatsActuelles.ts | 13 +- src/notion/statPropsPublishOptions.ts | 41 ----- src/notion/statPublishOptions.ts | 71 +++++++++ src/statistiques/ELStats.ts | 53 ++++--- src/statistiques/computeELPeriodStats.ts | 144 +++++++----------- src/statistiques/computeELStats.ts | 16 +- src/statistiques/computeELStatsAtDate.ts | 74 +++++++++ src/statistiques/computeELStatsOverPeriod.ts | 48 ++++++ ...putePourcentageEntreeApresMiseEnDemeure.ts | 17 +++ src/statistiques/computeStatsActuelles.ts | 45 ------ src/{statistiques => utils}/math/average.ts | 0 src/{statistiques => utils}/math/median.ts | 0 src/utils/math/percent.ts | 3 + src/utils/notNull.ts | 5 + 24 files changed, 515 insertions(+), 264 deletions(-) create mode 100644 src/data/ContexteEntreeDC.ts create mode 100644 src/data/EvenementFamille.ts create mode 100644 src/data/StatutFamille.ts create mode 100644 src/data/TypeEvenement.ts delete mode 100644 src/notion/statPropsPublishOptions.ts create mode 100644 src/notion/statPublishOptions.ts create mode 100644 src/statistiques/computeELStatsAtDate.ts create mode 100644 src/statistiques/computeELStatsOverPeriod.ts create mode 100644 src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts delete mode 100644 src/statistiques/computeStatsActuelles.ts rename src/{statistiques => utils}/math/average.ts (100%) rename src/{statistiques => utils}/math/median.ts (100%) create mode 100644 src/utils/math/percent.ts create mode 100644 src/utils/notNull.ts diff --git a/src/data/ContexteEntreeDC.ts b/src/data/ContexteEntreeDC.ts new file mode 100644 index 0000000..ff03280 --- /dev/null +++ b/src/data/ContexteEntreeDC.ts @@ -0,0 +1,6 @@ +export type ContexteEntreeDC = + | "Pas de demande (Plein droit)" + | "Pas de demande" + | "Après refus" + | "Après mise en demeure" + | "Après poursuite procureur"; diff --git a/src/data/EvenementFamille.ts b/src/data/EvenementFamille.ts new file mode 100644 index 0000000..341e0a8 --- /dev/null +++ b/src/data/EvenementFamille.ts @@ -0,0 +1,70 @@ +import { Period } from "../period/Period"; +import { isPeriodContaining } from "../period/isPeriodContaining"; +import { TypeEvenement } from "./TypeEvenement"; + +export type EvenementFamille = { + notionId: string; + notionIdFamille: string; + Évènement: string; + Date: Date | null; + Type: TypeEvenement; + "Enfants concernés": string; +}; + +export function isProcedurePenale(evenement: EvenementFamille): boolean { + return categorieEvenement[evenement.Type] === "Procédure Pénale"; +} + +export function isProcedureCivile(evenement: EvenementFamille): boolean { + return categorieEvenement[evenement.Type] === "Procédure Civile"; +} + +const categorieEvenement: { + [evt in TypeEvenement]: CategorieEvenement; +} = { + ["Récidive gendarmerie"]: "Procédure Pénale", + ["Appel du jugement"]: "Procédure Pénale", + ["Tribunal de police judiciaire"]: "Procédure Pénale", + ["Signalement au procureur"]: "Procédure Civile", // TBC + ["Mise en demeure de scolarisation"]: "Procédure Pénale", + ["Signalement"]: "Procédure Civile", + ["Audition gendarmerie / police"]: "Procédure Pénale", + ["Convocation procureur"]: "Procédure Pénale", + ["Audition procureur"]: "Procédure Pénale", + ["Composition pénale refusée"]: "Procédure Pénale", + ["Composition pénale acceptée"]: "Procédure Pénale", + ["Classement social sans suite"]: "Procédure Civile", + ["Classement pénal sans suite"]: "Procédure Pénale", + ["Enquête sociale"]: "Procédure Civile", + ["Information préoccupante"]: "Procédure Civile", + ["Juge pour enfants"]: "Procédure Civile", + ["Tribunal correctionnel"]: "Procédure Pénale", + ["Convocation CRPC"]: "Procédure Pénale", + ["Plaidoirie"]: "Procédure Pénale", + ["Audience CRPC"]: "Procédure Pénale", + ["Refus CRPC"]: "Procédure Pénale", + ["Audition des enfants"]: "Procédure Civile", + ["Assistance éducative"]: "Procédure Civile", + ["Contrôle forcé"]: "Autre", + ["Refus de contrôle"]: "Autre", + ["Rappel à la loi"]: "Procédure Pénale", + ["Passage police municipale"]: "Procédure Pénale", + ["Administrateur AD'HOC"]: "Autre", + ["Validation désobéissance"]: "Autre", +}; + +export type CategorieEvenement = + | "Procédure Pénale" + | "Procédure Civile" + | "Autre"; + +export function isEvenementInPeriod( + evt: EvenementFamille, + period: Period +): unknown { + return evt.Date && isPeriodContaining(period, evt.Date); +} + +export function isEvenementBefore(evt: EvenementFamille, date: Date): unknown { + return evt.Date !== null && evt.Date < date; +} diff --git a/src/data/Famille.ts b/src/data/Famille.ts index 04bd7f5..d27b72e 100644 --- a/src/data/Famille.ts +++ b/src/data/Famille.ts @@ -2,6 +2,9 @@ import { differenceInDays } from "date-fns"; import { Period } from "../period/Period"; import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping"; import { isPeriodContaining } from "../period/isPeriodContaining"; +import { ContexteEntreeDC } from "./ContexteEntreeDC"; +import { EvenementFamille } from "./EvenementFamille"; +import { StatutFamille } from "./StatutFamille"; export type Famille = { notionId: string; @@ -13,38 +16,6 @@ export type Famille = { Evenements: EvenementFamille[]; }; -export type EvenementFamille = { - notionId: string; - notionIdFamille: string; - Évènement: string; - Date: Date | null; - Type: TypeEvenement; - "Enfants concernés": string; -}; - -export type ContexteEntreeDC = - | "Pas de demande (Plein droit)" - | "Pas de demande" - | "Après refus" - | "Après mise en demeure" - | "Après poursuite procureur"; - -export type TypeEvenement = - | "Mise en demeure de scolarisation" - | "Composition pénale refusée" - | "Composition pénale acceptée"; - -export type StatutFamille = - | "Résistant.e" - | "Ex résistant·e·s" - | "À préciser" - | "Se questionne" - | "Désobéissence décidée" - | "Rédaction Déclaration" - | "Déclaration Validée - Attente éléments" - | "Abdandon" - | "Incompatible"; - export function periodOfResistance( family: Famille, atDate: Date = new Date(Date.now()) diff --git a/src/data/StatutFamille.ts b/src/data/StatutFamille.ts new file mode 100644 index 0000000..292824e --- /dev/null +++ b/src/data/StatutFamille.ts @@ -0,0 +1,10 @@ +export type StatutFamille = + | "Résistant.e" + | "Ex résistant·e·s" + | "À préciser" + | "Se questionne" + | "Désobéissence décidée" + | "Rédaction Déclaration" + | "Déclaration Validée - Attente éléments" + | "Abdandon" + | "Incompatible"; diff --git a/src/data/TypeEvenement.ts b/src/data/TypeEvenement.ts new file mode 100644 index 0000000..95c1f2b --- /dev/null +++ b/src/data/TypeEvenement.ts @@ -0,0 +1,30 @@ +export type TypeEvenement = + | "Récidive gendarmerie" + | "Appel du jugement" + | "Tribunal de police judiciaire" + | "Signalement au procureur" + | "Mise en demeure de scolarisation" + | "Signalement" + | "Audition gendarmerie / police" + | "Convocation procureur" + | "Audition procureur" + | "Composition pénale refusée" + | "Composition pénale acceptée" + | "Classement social sans suite" + | "Classement pénal sans suite" + | "Enquête sociale" + | "Information préoccupante" + | "Juge pour enfants" + | "Tribunal correctionnel" + | "Convocation CRPC" + | "Plaidoirie" + | "Audience CRPC" + | "Refus CRPC" + | "Audition des enfants" + | "Assistance éducative" + | "Contrôle forcé" + | "Refus de contrôle" + | "Rappel à la loi" + | "Passage police municipale" + | "Administrateur AD'HOC" + | "Validation désobéissance"; diff --git a/src/index.ts b/src/index.ts index 39b3736..828b331 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,7 @@ import { computeELStats } from "./statistiques/computeELStats"; const currentDate = new Date(Date.now()); console.log("Building statistics..."); - const resistantCountStats = computeELStats(families); + const resistantCountStats = computeELStats(families, currentDate); console.log("Publishing statistics..."); publishStatisticsToNotion(resistantCountStats, currentDate, notionClient); diff --git a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts index f577623..7e2f97e 100644 --- a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts +++ b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts @@ -1,12 +1,10 @@ import { Client, isFullPage } from "@notionhq/client"; import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; -import { - ContexteEntreeDC, - EvenementFamille, - Famille, - StatutFamille, - TypeEvenement, -} from "../../data/Famille"; +import { ContexteEntreeDC } from "../../data/ContexteEntreeDC"; +import { EvenementFamille } from "../../data/EvenementFamille"; +import { Famille } from "../../data/Famille"; +import { StatutFamille } from "../../data/StatutFamille"; +import { TypeEvenement } from "../../data/TypeEvenement"; import { datePropertyToDate } from "../utils/properties/datePropertyToDate"; import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId"; import { selectPropertyToText } from "../utils/properties/selectPropertyToText"; diff --git a/src/notion/publish/format/formatValue.ts b/src/notion/publish/format/formatValue.ts index 0abf439..6202079 100644 --- a/src/notion/publish/format/formatValue.ts +++ b/src/notion/publish/format/formatValue.ts @@ -1,4 +1,4 @@ -import { StatPublishOptions } from "../../statPropsPublishOptions"; +import { StatPublishOptions } from "../../statPublishOptions"; export function formatValue(value: number, publishOptions: StatPublishOptions) { const valueStr = value.toLocaleString("fr-FR", { diff --git a/src/notion/publish/format/formatValueWithEvol.ts b/src/notion/publish/format/formatValueWithEvol.ts index 14283cc..4bedd98 100644 --- a/src/notion/publish/format/formatValueWithEvol.ts +++ b/src/notion/publish/format/formatValueWithEvol.ts @@ -1,5 +1,5 @@ import { ValueWithEvol } from "../../../statistiques/ELStats"; -import { StatPublishOptions } from "../../statPropsPublishOptions"; +import { StatPublishOptions } from "../../statPublishOptions"; import { formatValue } from "./formatValue"; export function formatValueWithEvol( diff --git a/src/notion/publish/publishPeriodStats.ts b/src/notion/publish/publishPeriodStats.ts index f747cac..f9ff7cb 100644 --- a/src/notion/publish/publishPeriodStats.ts +++ b/src/notion/publish/publishPeriodStats.ts @@ -1,21 +1,30 @@ import { Client, isFullPage } from "@notionhq/client"; -import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; -import { ELPeriodStats, ValueWithEvol } from "../../statistiques/ELStats"; import { - StatPublishOptions, - statPropsPublishOptions, -} from "../statPropsPublishOptions"; + PageObjectResponse, + UpdateDatabaseParameters, +} from "@notionhq/client/build/src/api-endpoints"; +import { + ELStatsPeriod, + PeriodStatsValues, + ValueWithEvol, +} from "../../statistiques/ELStats"; +import { StatPublishOptions, statPublishOptions } from "../statPublishOptions"; import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; import { queryAllDbResults } from "../utils/queryAllDbResults"; import { removeBlocks } from "../utils/removeBlocks"; import { CreatePageProperties } from "../utils/types/CreatePageProperties"; import { formatValueWithEvol } from "./format/formatValueWithEvol"; +const periodeDbPropertyName = "Période"; + export async function publishPeriodStats( notionClient: Client, periodStatsDbId: string, - stats: ELPeriodStats[] + statsPeriods: ELStatsPeriod[] ) { + if (statsPeriods.length > 0) { + await updateDbProps(notionClient, periodStatsDbId, statsPeriods[0].stats); + } const periodRows = ( await queryAllDbResults(notionClient, { database_id: periodStatsDbId, @@ -24,11 +33,14 @@ export async function publishPeriodStats( const indexedPeriodRows: { [period: string]: PageObjectResponse } = Object.fromEntries( - periodRows.map((r) => [titlePropertyToText(r.properties, "Période"), r]) + periodRows.map((r) => [ + titlePropertyToText(r.properties, periodeDbPropertyName), + r, + ]) ); const indexedPeriodStats = Object.fromEntries( - stats.map((stat) => [stat.periodId, stat]) + statsPeriods.map((stat) => [stat.periodId, stat]) ); const rowIdsToDelete = Object.entries(indexedPeriodRows) @@ -76,15 +88,59 @@ export async function publishPeriodStats( } } +async function updateDbProps( + notionClient: Client, + periodStatsDbId: string, + stats: PeriodStatsValues +) { + const db = await notionClient.databases.retrieve({ + database_id: periodStatsDbId, + }); + const statsNotionProps: UpdateDatabaseParameters["properties"] = + Object.fromEntries( + (Object.keys(stats) as Array>).map( + (jsProp) => { + const publishOptions = statPublishOptions(jsProp); + return [ + publishOptions.notionPropName, + { + rich_text: {}, + }, + ]; + } + ) + ); + const propsToRemove = Object.fromEntries( + Object.keys(db.properties) + .filter( + (k) => + !Object.prototype.hasOwnProperty.call(statsNotionProps, k) && + k !== periodeDbPropertyName + ) + .map((k) => [k, null]) + ); + + await notionClient.databases.update({ + database_id: periodStatsDbId, + properties: { + [periodeDbPropertyName]: { + title: {}, + }, + ...statsNotionProps, + ...propsToRemove, + }, + }); +} + function buildRowPropertiesForUpsert( periodId: string, - stats: ELPeriodStats["stats"] + stats: PeriodStatsValues ): CreatePageProperties { const statsNotionProps: CreatePageProperties = Object.fromEntries( - (Object.keys(stats) as Array).map( + (Object.keys(stats) as Array>).map( (jsProp) => { const value: ValueWithEvol = stats[jsProp]; - const publishOptions = statPropsPublishOptions[jsProp]; + const publishOptions = statPublishOptions(jsProp); return [ publishOptions.notionPropName, valueWithEvolProp(value, publishOptions), @@ -93,7 +149,7 @@ function buildRowPropertiesForUpsert( ) ); return { - Période: { + [periodeDbPropertyName]: { title: [ { text: { diff --git a/src/notion/publish/publishStatsActuelles.ts b/src/notion/publish/publishStatsActuelles.ts index 2111de8..0c059de 100644 --- a/src/notion/publish/publishStatsActuelles.ts +++ b/src/notion/publish/publishStatsActuelles.ts @@ -1,10 +1,7 @@ import { Client, isFullBlock } from "@notionhq/client"; import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints"; -import { ELStatsActuelles } from "../../statistiques/ELStats"; -import { - StatPublishOptions, - statPropsPublishOptions, -} from "../statPropsPublishOptions"; +import { ELStatsAtDate } from "../../statistiques/ELStats"; +import { StatPublishOptions, statPublishOptions } from "../statPublishOptions"; import { listAllChildrenBlocks } from "../utils/listAllChildrenBlocks"; import { removeBlocks } from "../utils/removeBlocks"; import { richTextToPlainText } from "../utils/text/richTextToPlainText"; @@ -13,13 +10,13 @@ import { currentStatsHeading, statsPageId } from "./publishStatisticsToNotion"; export async function publishStatsActuelles( notionClient: Client, - statsActuelles: ELStatsActuelles + statsActuelles: ELStatsAtDate ) { const newBlocks = ( - Object.keys(statsActuelles) as Array + Object.keys(statsActuelles) as Array ).map((jsProp) => { const value: number = statsActuelles[jsProp]; - const publishOptions = statPropsPublishOptions[jsProp]; + const publishOptions = statPublishOptions(jsProp); return currentStatBlock(value, publishOptions); }); await updateStatsActuellesBlocks(notionClient, newBlocks); diff --git a/src/notion/statPropsPublishOptions.ts b/src/notion/statPropsPublishOptions.ts deleted file mode 100644 index 2590e77..0000000 --- a/src/notion/statPropsPublishOptions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ELPeriodStats, ELStatsActuelles } from "../statistiques/ELStats"; - -export const statPropsPublishOptions: { - [jsPropName in statsPropNames]: StatPublishOptions; -} = { - nbFamilleResistantes: { - notionPropName: "Nb familles résistantes", - }, - nbFamilleResistantesOrEx: { - notionPropName: "Nb familles résistantes ou ex-résistantes", - }, - dureeResistanceMoyenne: { - notionPropName: "Durée résistance moyenne", - unit: "j", - }, - dureeResistanceMediane: { - notionPropName: "Durée résistance médiane", - unit: "j", - }, - nbFamillesMisesEnDemeure: { - notionPropName: "Nb familles mises en demeure", - }, - pourcentageFamillesMisesEnDemeure: { - notionPropName: "Pourcentage de familles mises en demeure", - unit: "%", - }, - pourcentageEntreeApresMiseEnDemeure: { - notionPropName: "Pourcentage d'entrées après mises en demeure", - unit: "%", - }, -}; -export type statsPropNames = - | keyof ELPeriodStats["stats"] - | keyof ELStatsActuelles; -export type StatPublishOptions = { - notionPropName: string; - unit?: string; - valueMaxFractioDigits?: number; - evolMaxFractioDigits?: number; - evolPctMaxFractioDigits?: number; -}; diff --git a/src/notion/statPublishOptions.ts b/src/notion/statPublishOptions.ts new file mode 100644 index 0000000..e243c72 --- /dev/null +++ b/src/notion/statPublishOptions.ts @@ -0,0 +1,71 @@ +import { AllStatsPropNames } from "../statistiques/ELStats"; + +export function statPublishOptions( + statJsPropName: AllStatsPropNames +): StatPublishOptions { + return statPropsPublishOptions[statJsPropName]; +} +const statPropsPublishOptions: { + [jsPropName in AllStatsPropNames]: StatPublishOptions; +} = { + nbFamilleResistantes: { + notionPropName: "Nb familles résistantes", + }, + + nbFamilleResistantesSurPeriode: { + notionPropName: "Nb familles résistantes sur période", + }, + nbFamilleResistantesOrEx: { + notionPropName: "Nb familles résistantes ou ex-résistantes", + }, + dureeResistanceMoyenne: { + notionPropName: "Durée résistance moyenne", + unit: "j", + }, + dureeResistanceMediane: { + notionPropName: "Durée résistance médiane", + unit: "j", + }, + nbFamillesMisesEnDemeure: { + notionPropName: "Nb familles mises en demeure", + }, + pourcentageFamillesMisesEnDemeure: { + notionPropName: "% de familles mises en demeure", + unit: "%", + }, + pourcentageEntreeApresMiseEnDemeure: { + notionPropName: "% d'entrées après mises en demeure", + unit: "%", + }, + pourcentageEntreeApresMiseEnDemeureSurPeriode: { + notionPropName: "% d'entrées après mises en demeure sur période", + }, + nbFamillesProcedurePenale: { + notionPropName: "Nb familles avec procédure pénale", + }, + pourcentageFamillesProcedurePenale: { + notionPropName: "% familles avec procédure pénale", + unit: "%", + }, + nbFamilleAvecProcedurePenaleSurPeriode: { + notionPropName: "Nb familles avec procédure pénale sur période", + }, + + nbFamillesProcedureCivile: { + notionPropName: "Nb familles avec procédure civile", + }, + pourcentageFamillesProcedureCivile: { + notionPropName: "% familles avec procédure civile", + unit: "%", + }, + nbFamilleAvecProcedureCivileSurPeriode: { + notionPropName: "Nb familles avec procédure civile sur période", + }, +}; +export type StatPublishOptions = { + notionPropName: string; + unit?: string; + valueMaxFractioDigits?: number; + evolMaxFractioDigits?: number; + evolPctMaxFractioDigits?: number; +}; diff --git a/src/statistiques/ELStats.ts b/src/statistiques/ELStats.ts index 23e8aec..1b6e32e 100644 --- a/src/statistiques/ELStats.ts +++ b/src/statistiques/ELStats.ts @@ -1,29 +1,36 @@ export type ELStats = { - actuelles: ELStatsActuelles; - annees: ELPeriodStats[]; + actuelles: ELStatsAtDate; + annees: ELStatsPeriod[]; - mois: ELPeriodStats[]; -}; -export type ELStatsActuelles = { - nbFamilleResistantes: number; - nbFamilleResistantesOrEx: number; - dureeResistanceMoyenne: number; - dureeResistanceMediane: number; - nbFamillesMisesEnDemeure: number; - pourcentageFamillesMisesEnDemeure: number; - pourcentageEntreeApresMiseEnDemeure: number; + mois: ELStatsPeriod[]; }; -export type ELPeriodStats = { +export type ELStatsAtDate = { + nbFamilleResistantes: V; + nbFamilleResistantesOrEx: V; + dureeResistanceMoyenne: V; + dureeResistanceMediane: V; + nbFamillesMisesEnDemeure: V; + pourcentageFamillesMisesEnDemeure: V; + pourcentageEntreeApresMiseEnDemeure: V; + nbFamillesProcedurePenale: V; + pourcentageFamillesProcedurePenale: V; + nbFamillesProcedureCivile: V; + pourcentageFamillesProcedureCivile: V; +}; + +export type ELStatsPeriod = { periodId: string; - stats: { - nbFamilleResistantes: ValueWithEvol; - nbFamilleResistantesOrEx: ValueWithEvol; - dureeResistanceMoyenne: ValueWithEvol; - dureeResistanceMediane: ValueWithEvol; - nbFamillesMisesEnDemeure: ValueWithEvol; - pourcentageEntreeApresMiseEnDemeure: ValueWithEvol; - }; + stats: PeriodStatsValues; +}; + +export type PeriodStatsValues = ELStatsAtDate | ELStatsOverPeriod; + +export type ELStatsOverPeriod = { + pourcentageEntreeApresMiseEnDemeureSurPeriode: V; + nbFamilleResistantesSurPeriode: V; + nbFamilleAvecProcedurePenaleSurPeriode: V; + nbFamilleAvecProcedureCivileSurPeriode: V; }; export type ValueWithEvol = { @@ -31,3 +38,7 @@ export type ValueWithEvol = { evol: number; evolPercent: number; }; + +export type AllStatsPropNames = + | keyof ELStatsOverPeriod + | keyof ELStatsAtDate; diff --git a/src/statistiques/computeELPeriodStats.ts b/src/statistiques/computeELPeriodStats.ts index e4739cb..cf299e1 100644 --- a/src/statistiques/computeELPeriodStats.ts +++ b/src/statistiques/computeELPeriodStats.ts @@ -1,94 +1,68 @@ -import { - Famille, - dureeResistanceInDays, - isResistantOverPeriod, - periodOfResistance, -} from "../data/Famille"; +import { Famille } from "../data/Famille"; import { IdentifiedPeriod } from "../period/IdentifiedPeriod"; -import { isPeriodContaining } from "../period/isPeriodContaining"; -import { ELPeriodStats, ValueWithEvol } from "./ELStats"; -import { average } from "./math/average"; -import { median } from "./math/median"; +import { Period } from "../period/Period"; +import { + ELStatsOverPeriod, + ELStatsPeriod, + PeriodStatsValues, + ValueWithEvol, +} from "./ELStats"; +import { computeELStatsAtDate } from "./computeELStatsAtDate"; +import { computeELStatsOverPeriod } from "./computeELStatsOverPeriod"; export function computeELPeriodStats( familles: Famille[], periods: IdentifiedPeriod[] -): ELPeriodStats[] { - const periodStats: ELPeriodStats[] = []; - let previousELPeriodStats: ELPeriodStats["stats"] | null = null; - for (const period of periods) { - const periodEndOrNow = - period.end.getTime() > Date.now() ? new Date(Date.now()) : period.end; +): ELStatsPeriod[] { + let previousPeriodStatNumberValues: PeriodStatsValues | null = null; + return periods.map((period) => { + const periodStatNumberValues: PeriodStatsValues = + computePeriodStatsNumberValues(familles, period); - const nbFamilleResistantes = familles.filter((famille) => - isResistantOverPeriod(famille, period) - ).length; - - const nbFamilleResistantesOrEx = familles.filter((famille) => { - const por = periodOfResistance(famille, periodEndOrNow); - return por !== null && por.start < periodEndOrNow; - }).length; - - const dureesResistances = familles - .map((famille) => dureeResistanceInDays(famille, periodEndOrNow)) - .filter(notNull); - const dureeResistanceMediane = Math.round(median(dureesResistances)); - const dureeResistanceMoyenne = Math.round(average(dureesResistances)); - - const nbFamillesMiseEnDemeure = familles.filter((famille) => - famille.Evenements.find( - (e) => - e.Type === "Mise en demeure de scolarisation" && - e.Date && - isPeriodContaining(period, e.Date) - ) - ).length; - const famillesEntreesOverPeriod = familles.filter((f) => { - const por = periodOfResistance(f); - return por != null && isPeriodContaining(period, por.start); - }); - const entreeApresMiseEnDemeure = famillesEntreesOverPeriod.filter( - (f) => - f.ContexteEntree === "Après mise en demeure" || - f.ContexteEntree === "Après poursuite procureur" + // Compute evol + const statsWithEvol = computeStatsEvol( + periodStatNumberValues, + previousPeriodStatNumberValues ); - const pourcentageEntreeApresMiseEnDemeure = - (100 * entreeApresMiseEnDemeure.length) / - famillesEntreesOverPeriod.length; - - const stats: ELPeriodStats = { + previousPeriodStatNumberValues = periodStatNumberValues; + const periodStats: ELStatsPeriod = { periodId: period.id, - stats: { - nbFamilleResistantes: valueWithEvol( - nbFamilleResistantes, - previousELPeriodStats?.nbFamilleResistantes.value - ), - nbFamilleResistantesOrEx: valueWithEvol( - nbFamilleResistantesOrEx, - previousELPeriodStats?.nbFamilleResistantesOrEx.value - ), - dureeResistanceMediane: valueWithEvol( - dureeResistanceMediane, - previousELPeriodStats?.dureeResistanceMediane.value - ), - dureeResistanceMoyenne: valueWithEvol( - dureeResistanceMoyenne, - previousELPeriodStats?.dureeResistanceMoyenne.value - ), - nbFamillesMisesEnDemeure: valueWithEvol( - nbFamillesMiseEnDemeure, - previousELPeriodStats?.nbFamillesMisesEnDemeure.value - ), - pourcentageEntreeApresMiseEnDemeure: valueWithEvol( - pourcentageEntreeApresMiseEnDemeure, - previousELPeriodStats?.pourcentageEntreeApresMiseEnDemeure.value - ), - }, + stats: statsWithEvol, }; - periodStats.push(stats); - previousELPeriodStats = stats?.stats; - } - return periodStats; + return periodStats; + }); +} + +function computePeriodStatsNumberValues( + familles: Famille[], + period: Period +): PeriodStatsValues { + const statsAtPeriodEnd = computeELStatsAtDate(familles, period.end); + + const statsOverPeriod: ELStatsOverPeriod = computeELStatsOverPeriod( + familles, + period + ); + return { + ...statsAtPeriodEnd, + ...statsOverPeriod, + }; +} + +function computeStatsEvol( + periodStatNumberValues: PeriodStatsValues, + previousPeriodStatNumberValues: PeriodStatsValues | null | undefined +): PeriodStatsValues { + return Object.fromEntries( + ( + Object.entries(periodStatNumberValues) as Array< + [prop: keyof PeriodStatsValues, number] + > + ).map(([k, v]) => [ + k, + valueWithEvol(v, previousPeriodStatNumberValues?.[k]), + ]) + ) as PeriodStatsValues; } function valueWithEvol( @@ -111,9 +85,3 @@ function evol(current: number, previous: number | undefined): number { if (previous === undefined) return NaN; return current - previous; } - -// Typescript is not able to infer this inline see -// https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array -function notNull(value: TValue | null): value is TValue { - return value !== null; -} diff --git a/src/statistiques/computeELStats.ts b/src/statistiques/computeELStats.ts index 2f72b1d..1d6b7b4 100644 --- a/src/statistiques/computeELStats.ts +++ b/src/statistiques/computeELStats.ts @@ -1,17 +1,19 @@ import { Famille } from "../data/Famille"; import { ELStats } from "./ELStats"; import { computeELPeriodStats } from "./computeELPeriodStats"; -import { computeStatsActuelles } from "./computeStatsActuelles"; +import { computeELStatsAtDate } from "./computeELStatsAtDate"; import { generateELMonths } from "./generateELMonths"; import { generateELYears } from "./generateELYears"; -export function computeELStats(families: Famille[]): ELStats { - const actuelles = computeStatsActuelles(families); - const elYears = generateELYears(); - const yearsStats = computeELPeriodStats(families, elYears); +export function computeELStats( + families: Famille[], + currentDate: Date +): ELStats { + const actuelles = computeELStatsAtDate(families, currentDate); - const months = generateELMonths(); - const monthsStats = computeELPeriodStats(families, months); + const yearsStats = computeELPeriodStats(families, generateELYears()); + + const monthsStats = computeELPeriodStats(families, generateELMonths()); return { actuelles: actuelles, diff --git a/src/statistiques/computeELStatsAtDate.ts b/src/statistiques/computeELStatsAtDate.ts new file mode 100644 index 0000000..cb18c21 --- /dev/null +++ b/src/statistiques/computeELStatsAtDate.ts @@ -0,0 +1,74 @@ +import { isEvenementBefore, isProcedurePenale } from "../data/EvenementFamille"; +import { + Famille, + dureeResistanceInDays, + isExResistant, + isResistant, +} from "../data/Famille"; +import { average } from "../utils/math/average"; +import { median } from "../utils/math/median"; +import { percent } from "../utils/math/percent"; +import { notNull } from "../utils/notNull"; +import { ELStatsAtDate } from "./ELStats"; +import { computePourcentageEntreeApresMiseEnDemeure } from "./computePourcentageEntreeApresMiseEnDemeure"; + +export function computeELStatsAtDate( + familles: Famille[], + asOfDate: Date +): ELStatsAtDate { + const familleResistantes = familles.filter((f) => isResistant(f, asOfDate)); + const familleResistantesOrEx = familles.filter( + (famille) => + isResistant(famille, asOfDate) || isExResistant(famille, asOfDate) + ); + const dureesResistances = familles + .map((famille) => dureeResistanceInDays(famille, asOfDate)) + .filter(notNull); + + const nbFamillesMiseEnDemeure = familleResistantesOrEx.filter((f) => + f.Evenements.find((e) => e.Type === "Mise en demeure de scolarisation") + ).length; + const pourcentageFamillesMisesEnDemeure = percent( + nbFamillesMiseEnDemeure, + familleResistantesOrEx.length + ); + + const pourcentageEntreeApresMiseEnDemeure = + computePourcentageEntreeApresMiseEnDemeure(familleResistantesOrEx); + + const famillesAvecProcedurePenale = familleResistantesOrEx.filter((famille) => + famille.Evenements.find( + (evt) => isProcedurePenale(evt) && isEvenementBefore(evt, asOfDate) + ) + ); + const pourcentageFamillesProcedurePenale = percent( + famillesAvecProcedurePenale.length, + familleResistantesOrEx.length + ); + const famillesAvecProcedureCivile = familleResistantesOrEx.filter((famille) => + famille.Evenements.find( + (evt) => isProcedurePenale(evt) && isEvenementBefore(evt, asOfDate) + ) + ); + const pourcentageFamillesProcedureCivile = percent( + famillesAvecProcedureCivile.length, + familleResistantesOrEx.length + ); + + const actuelles: ELStatsAtDate = { + nbFamilleResistantes: familleResistantes.length, + nbFamilleResistantesOrEx: familleResistantesOrEx.length, + + dureeResistanceMoyenne: average(dureesResistances), + dureeResistanceMediane: median(dureesResistances), + + nbFamillesMisesEnDemeure: nbFamillesMiseEnDemeure, + pourcentageFamillesMisesEnDemeure: pourcentageFamillesMisesEnDemeure, + pourcentageEntreeApresMiseEnDemeure: pourcentageEntreeApresMiseEnDemeure, + nbFamillesProcedurePenale: famillesAvecProcedurePenale.length, + pourcentageFamillesProcedurePenale: pourcentageFamillesProcedurePenale, + nbFamillesProcedureCivile: famillesAvecProcedureCivile.length, + pourcentageFamillesProcedureCivile: pourcentageFamillesProcedureCivile, + }; + return actuelles; +} diff --git a/src/statistiques/computeELStatsOverPeriod.ts b/src/statistiques/computeELStatsOverPeriod.ts new file mode 100644 index 0000000..ed040e4 --- /dev/null +++ b/src/statistiques/computeELStatsOverPeriod.ts @@ -0,0 +1,48 @@ +import { + isEvenementInPeriod, + isProcedurePenale, +} from "../data/EvenementFamille"; +import { + Famille, + isResistantOverPeriod, + periodOfResistance, +} from "../data/Famille"; +import { Period } from "../period/Period"; +import { isPeriodContaining } from "../period/isPeriodContaining"; +import { ELStatsOverPeriod } from "./ELStats"; +import { computePourcentageEntreeApresMiseEnDemeure } from "./computePourcentageEntreeApresMiseEnDemeure"; + +export function computeELStatsOverPeriod( + familles: Famille[], + period: Period +): ELStatsOverPeriod { + const famillesEntreesSurPeriode = familles.filter((f) => { + const por = periodOfResistance(f); + return por != null && isPeriodContaining(period, por.start); + }); + + const nbFamilleResistantesSurPeriode = familles.filter((famille) => + isResistantOverPeriod(famille, period) + ).length; + const pourcentageEntreeApresMiseEnDemeureSurPeriode = + computePourcentageEntreeApresMiseEnDemeure(famillesEntreesSurPeriode); + const familleAvecProcedurePenaleSurPeriode = familles.filter((famille) => + famille.Evenements.find( + (evt) => isProcedurePenale(evt) && isEvenementInPeriod(evt, period) + ) + ); + const familleAvecProcedureCivileSurPeriode = familles.filter((famille) => + famille.Evenements.find( + (evt) => isProcedurePenale(evt) && isEvenementInPeriod(evt, period) + ) + ); + return { + nbFamilleResistantesSurPeriode, + pourcentageEntreeApresMiseEnDemeureSurPeriode, + nbFamilleAvecProcedurePenaleSurPeriode: + familleAvecProcedurePenaleSurPeriode.length, + + nbFamilleAvecProcedureCivileSurPeriode: + familleAvecProcedureCivileSurPeriode.length, + }; +} diff --git a/src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts b/src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts new file mode 100644 index 0000000..3b19a51 --- /dev/null +++ b/src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts @@ -0,0 +1,17 @@ +import { Famille } from "../data/Famille"; +import { percent } from "../utils/math/percent"; + +export function computePourcentageEntreeApresMiseEnDemeure( + familles: Famille[] +) { + const entreeApresMiseEnDemeure = familles.filter( + (f) => + f.ContexteEntree === "Après mise en demeure" || + f.ContexteEntree === "Après poursuite procureur" + ); + const pourcentageEntreeApresMiseEnDemeure = percent( + entreeApresMiseEnDemeure.length, + familles.length + ); + return pourcentageEntreeApresMiseEnDemeure; +} diff --git a/src/statistiques/computeStatsActuelles.ts b/src/statistiques/computeStatsActuelles.ts deleted file mode 100644 index 0db935a..0000000 --- a/src/statistiques/computeStatsActuelles.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Famille, - isExResistant, - isResistant, - periodOfResistance, -} from "../data/Famille"; -import { daysInPeriod } from "../period/daysInPeriod"; -import { ELStatsActuelles } from "./ELStats"; -import { average } from "./math/average"; -import { median } from "./math/median"; - -export function computeStatsActuelles(familles: Famille[]): ELStatsActuelles { - const resistantsCount = familles.filter((f) => isResistant(f)).length; - const resistantsOrEx = familles.filter( - (f) => isResistant(f) || isExResistant(f) - ); - const durations = resistantsOrEx.map((f) => - daysInPeriod(periodOfResistance(f)!) - ); - const dureeMoyenne = average(durations); - const dureeMediane = median(durations); - - const familleAvecMiseEnDemeure = resistantsOrEx.filter((f) => - f.Evenements.find((e) => e.Type === "Mise en demeure de scolarisation") - ); - - const entreeApresMiseEnDemeure = resistantsOrEx.filter( - (f) => - f.ContexteEntree === "Après mise en demeure" || - f.ContexteEntree === "Après poursuite procureur" - ); - - const actuelles: ELStatsActuelles = { - nbFamilleResistantes: resistantsCount, - nbFamilleResistantesOrEx: resistantsOrEx.length, - dureeResistanceMoyenne: dureeMoyenne, - dureeResistanceMediane: dureeMediane, - nbFamillesMisesEnDemeure: familleAvecMiseEnDemeure.length, - pourcentageFamillesMisesEnDemeure: - (100 * familleAvecMiseEnDemeure.length) / resistantsOrEx.length, - pourcentageEntreeApresMiseEnDemeure: - (100 * entreeApresMiseEnDemeure.length) / resistantsOrEx.length, - }; - return actuelles; -} diff --git a/src/statistiques/math/average.ts b/src/utils/math/average.ts similarity index 100% rename from src/statistiques/math/average.ts rename to src/utils/math/average.ts diff --git a/src/statistiques/math/median.ts b/src/utils/math/median.ts similarity index 100% rename from src/statistiques/math/median.ts rename to src/utils/math/median.ts diff --git a/src/utils/math/percent.ts b/src/utils/math/percent.ts new file mode 100644 index 0000000..4ebf237 --- /dev/null +++ b/src/utils/math/percent.ts @@ -0,0 +1,3 @@ +export function percent(value: number, total: number): number { + return (100 * value) / total; +} diff --git a/src/utils/notNull.ts b/src/utils/notNull.ts new file mode 100644 index 0000000..b57855f --- /dev/null +++ b/src/utils/notNull.ts @@ -0,0 +1,5 @@ +// Typescript is not able to infer this inline see +// https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array +export function notNull(value: TValue | null): value is TValue { + return value !== null; +}