feat: Revue des stats sur + draft pénale et civile

This commit is contained in:
sebastien.arod@gmail.com 2024-06-05 10:27:27 +02:00
parent af0fba621a
commit f328137c30
24 changed files with 515 additions and 264 deletions

View file

@ -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";

View file

@ -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;
}

View file

@ -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())

10
src/data/StatutFamille.ts Normal file
View file

@ -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";

30
src/data/TypeEvenement.ts Normal file
View file

@ -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";

View file

@ -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);

View file

@ -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";

View file

@ -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", {

View file

@ -1,5 +1,5 @@
import { ValueWithEvol } from "../../../statistiques/ELStats";
import { StatPublishOptions } from "../../statPropsPublishOptions";
import { StatPublishOptions } from "../../statPublishOptions";
import { formatValue } from "./formatValue";
export function formatValueWithEvol(

View file

@ -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<ValueWithEvol>
) {
const db = await notionClient.databases.retrieve({
database_id: periodStatsDbId,
});
const statsNotionProps: UpdateDatabaseParameters["properties"] =
Object.fromEntries(
(Object.keys(stats) as Array<keyof PeriodStatsValues<ValueWithEvol>>).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<ValueWithEvol>
): CreatePageProperties {
const statsNotionProps: CreatePageProperties = Object.fromEntries(
(Object.keys(stats) as Array<keyof ELPeriodStats["stats"]>).map(
(Object.keys(stats) as Array<keyof PeriodStatsValues<ValueWithEvol>>).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: {

View file

@ -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<number>
) {
const newBlocks = (
Object.keys(statsActuelles) as Array<keyof ELStatsActuelles>
Object.keys(statsActuelles) as Array<keyof typeof statsActuelles>
).map((jsProp) => {
const value: number = statsActuelles[jsProp];
const publishOptions = statPropsPublishOptions[jsProp];
const publishOptions = statPublishOptions(jsProp);
return currentStatBlock(value, publishOptions);
});
await updateStatsActuellesBlocks(notionClient, newBlocks);

View file

@ -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;
};

View file

@ -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;
};

View file

@ -1,29 +1,36 @@
export type ELStats = {
actuelles: ELStatsActuelles;
annees: ELPeriodStats[];
actuelles: ELStatsAtDate<number>;
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<V> = {
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<ValueWithEvol>;
};
export type PeriodStatsValues<V> = ELStatsAtDate<V> | ELStatsOverPeriod<V>;
export type ELStatsOverPeriod<V> = {
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<number>
| keyof ELStatsAtDate<number>;

View file

@ -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<number> | null = null;
return periods.map((period) => {
const periodStatNumberValues: PeriodStatsValues<number> =
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;
});
}
function computePeriodStatsNumberValues(
familles: Famille[],
period: Period
): PeriodStatsValues<number> {
const statsAtPeriodEnd = computeELStatsAtDate(familles, period.end);
const statsOverPeriod: ELStatsOverPeriod<number> = computeELStatsOverPeriod(
familles,
period
);
return {
...statsAtPeriodEnd,
...statsOverPeriod,
};
}
function computeStatsEvol(
periodStatNumberValues: PeriodStatsValues<number>,
previousPeriodStatNumberValues: PeriodStatsValues<number> | null | undefined
): PeriodStatsValues<ValueWithEvol> {
return Object.fromEntries(
(
Object.entries(periodStatNumberValues) as Array<
[prop: keyof PeriodStatsValues<number>, number]
>
).map(([k, v]) => [
k,
valueWithEvol(v, previousPeriodStatNumberValues?.[k]),
])
) as PeriodStatsValues<ValueWithEvol>;
}
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<TValue>(value: TValue | null): value is TValue {
return value !== null;
}

View file

@ -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,

View file

@ -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<number> {
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<number> = {
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;
}

View file

@ -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<number> {
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,
};
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,3 @@
export function percent(value: number, total: number): number {
return (100 * value) / total;
}

5
src/utils/notNull.ts Normal file
View file

@ -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<TValue>(value: TValue | null): value is TValue {
return value !== null;
}