feat: stats pénales par dept

This commit is contained in:
Sébastien Arod 2025-06-13 18:22:40 +02:00
parent f2d2744b6d
commit 376a79ae85
12 changed files with 235 additions and 108 deletions

View file

@ -0,0 +1,51 @@
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
import { ValueFormatOptions } from "../../../format/ValueFormatOptions";
import {
StatDesc,
StatType,
isStatGroupListDesc,
isStatGroupDesc,
isSingleValueStatDesc,
StatsData,
isMultiValueStatDesc,
} from "../../../statistiques/v2/desc/StatsDesc";
import { createMultiValueStatListItemBlock } from "./createMultiValueStatListItemBlock";
import { createSingleValueStatListItemBlock } from "./createSingleValueStatListItemBlock";
import { createStatGroupListItemBlock } from "./createStatGroupListItemBlock";
import { createStatGroupListListItemBlock } from "./createStatGroupListListItemBlock";
export function createStatBlock<T extends StatDesc>(
statDesc: T,
statData: StatType<T>
): BlockObjectRequest {
if (isStatGroupListDesc(statDesc)) {
return createStatGroupListListItemBlock(
statDesc,
statData as StatType<typeof statDesc>
);
}
if (isStatGroupDesc(statDesc)) {
return createStatGroupListItemBlock(
statDesc,
statData as StatType<typeof statDesc>
);
}
if (isSingleValueStatDesc(statDesc)) {
return createSingleValueStatListItemBlock(
statDesc.label,
statDesc as ValueFormatOptions,
statData as StatsData
);
}
if (isMultiValueStatDesc(statDesc)) {
return createMultiValueStatListItemBlock(
statDesc,
statData as Record<string, StatsData>
);
}
throw new Error("Unsupported stat descriptor type");
}

View file

@ -1,21 +1,15 @@
import { import {
isMultiValueStatDesc,
isSingleValueStatDesc,
isStatGroupDesc,
StatGroupDesc, StatGroupDesc,
StatsData, StatType,
StatsType,
} from "../../../statistiques/v2/desc/StatsDesc"; } from "../../../statistiques/v2/desc/StatsDesc";
import { createSingleValueStatListItemBlock } from "./createSingleValueStatListItemBlock";
import { BulletedListItemBlockObjectRequest } from "../blocks/BulletedListItemBlockObjectRequest"; import { BulletedListItemBlockObjectRequest } from "../blocks/BulletedListItemBlockObjectRequest";
import { BulletedListItemChildren } from "../blocks/BulletedListItemChildren"; import { BulletedListItemChildren } from "../blocks/BulletedListItemChildren";
import { createMultiValueStatListItemBlock } from "./createMultiValueStatListItemBlock";
import { ValueFormatOptions } from "../../../format/ValueFormatOptions";
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints"; import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
import { createStatBlock } from "./createStatBlock";
export function createStatGroupListItemBlock<D extends StatGroupDesc>( export function createStatGroupListItemBlock<D extends StatGroupDesc>(
descriptor: D, descriptor: D,
stats: StatsType<D> stats: StatType<D>
): BulletedListItemBlockObjectRequest { ): BulletedListItemBlockObjectRequest {
return { return {
bulleted_list_item: { bulleted_list_item: {
@ -36,32 +30,12 @@ export function createStatGroupListItemBlock<D extends StatGroupDesc>(
export function createStatGroupChildrenListItemBlock<D extends StatGroupDesc>( export function createStatGroupChildrenListItemBlock<D extends StatGroupDesc>(
descriptor: D, descriptor: D,
stats: StatsType<D> stats: StatType<D>
): BlockObjectRequest[] { ): BlockObjectRequest[] {
return Object.keys(descriptor.stats).map((statName) => { return Object.keys(descriptor.stats).map((statName) => {
const childStatDesc = descriptor.stats[statName]; const childStatDesc = descriptor.stats[statName];
const childStatData = stats[statName]; const childStatData = stats[statName];
if (isStatGroupDesc(childStatDesc)) { return createStatBlock(childStatDesc, childStatData);
return createStatGroupListItemBlock(
childStatDesc,
childStatData as StatsType<typeof childStatDesc>
);
}
if (isSingleValueStatDesc(childStatDesc)) {
return createSingleValueStatListItemBlock(
childStatDesc.label,
childStatDesc as ValueFormatOptions,
childStatData as StatsData
);
}
if (isMultiValueStatDesc(childStatDesc)) {
return createMultiValueStatListItemBlock(
childStatDesc,
childStatData as Record<string, StatsData>
);
}
throw "Mussing case";
}); });
} }

View file

@ -0,0 +1,45 @@
import {
StatGroupListDesc,
StatType,
} from "../../../statistiques/v2/desc/StatsDesc";
import { BulletedListItemBlockObjectRequest } from "../blocks/BulletedListItemBlockObjectRequest";
import { BulletedListItemChildren } from "../blocks/BulletedListItemChildren";
import { createStatBlock } from "./createStatBlock";
export function createStatGroupListListItemBlock<D extends StatGroupListDesc>(
descriptor: D,
statsDictionnaryData: StatType<D>
): BulletedListItemBlockObjectRequest {
return {
bulleted_list_item: {
rich_text: [
{
text: {
content: descriptor.label,
},
},
],
children: Object.entries(statsDictionnaryData).map(
([keyName, statData]) => {
return {
bulleted_list_item: {
rich_text: [
{
text: {
content: keyName,
},
},
],
children: Object.keys(descriptor.stats).map((statName) => {
const childStatDesc = descriptor.stats[statName];
const childStatData = statData[statName];
return createStatBlock(childStatDesc, childStatData);
}) as BulletedListItemChildren,
},
};
}
) as BulletedListItemChildren,
},
};
}

View file

@ -1,7 +1,7 @@
import { Client } from "@notionhq/client"; import { Client } from "@notionhq/client";
import { import {
StatGroupDesc, StatGroupDesc,
StatsType, StatType,
} from "../../../statistiques/v2/desc/StatsDesc"; } from "../../../statistiques/v2/desc/StatsDesc";
import { createStatGroupChildrenListItemBlock } from "./createStatGroupListItemBlock"; import { createStatGroupChildrenListItemBlock } from "./createStatGroupListItemBlock";
import { updatePageContent } from "./updatePageContent"; import { updatePageContent } from "./updatePageContent";
@ -12,7 +12,7 @@ export async function publishStatsToPage<D extends StatGroupDesc>(
statsPageId: string, statsPageId: string,
pageHeader: string, pageHeader: string,
descriptor: D, descriptor: D,
stats: StatsType<D> stats: StatType<D>
) { ) {
const statsBlocks = createStatGroupChildrenListItemBlock(descriptor, stats); const statsBlocks = createStatGroupChildrenListItemBlock(descriptor, stats);

View file

@ -1,5 +1,5 @@
import { statsPenalesDesc } from "./penales/StatsPenales"; import { statsPenalesDesc } from "./penales/StatsPenales";
import { StatsType } from "./desc/StatsDesc"; import { StatType } from "./desc/StatsDesc";
export const eLStatsV2Desc = { export const eLStatsV2Desc = {
label: "Stats v2", label: "Stats v2",
@ -8,4 +8,4 @@ export const eLStatsV2Desc = {
}, },
}; };
export type ELStatsV2 = StatsType<typeof eLStatsV2Desc>; export type ELStatsV2 = StatType<typeof eLStatsV2Desc>;

View file

@ -1,18 +1,18 @@
import { StatsType } from "../desc/StatsDesc"; import { StatType } from "../desc/StatsDesc";
export const statsAutresDesc = { export const statsAutresDesc = {
label: "Stats Sociales", label: "Stats Sociales",
stats: { stats: {
nbFamillesSuppressionCAF: { nbFamillesSuppressionCAF: {
label: "Nb Familles avec une suppression CAF" label: "Nb Familles avec une suppression CAF",
}, },
nbFamillesControleFiscal: { nbFamillesControleFiscal: {
label: "Nb Familles ayant eu un contrôle fiscal" label: "Nb Familles ayant eu un contrôle fiscal",
}, },
nbFamillesControleURSAFF: { nbFamillesControleURSAFF: {
label: "Nb Familles ayant eu un contrôle URSAFF" label: "Nb Familles ayant eu un contrôle URSAFF",
}, },
}, },
} as const; } as const;
export type StatsAutres = StatsType<typeof statsAutresDesc>; export type StatsAutres = StatType<typeof statsAutresDesc>;

View file

@ -1,53 +0,0 @@
import { ValueFormatOptions } from "../../../format/ValueFormatOptions";
type StatDescTypes = StatGroupDesc | StatDesc | MultiValueStatDesc;
/**
* Descripteur d'une Section de stat
*/
export type StatGroupDesc = {
label: string;
desc?: string;
stats: { [propName: string]: StatDescTypes };
};
export function isStatGroupDesc(x: StatDescTypes): x is StatGroupDesc {
if (!("stats" in x) || x.stats === null || typeof x.stats !== "object") {
return false;
}
return true;
}
/**
* Descripteur d'une valeur unique
*/
export type StatDesc = {
label: string;
} & ValueFormatOptions;
export function isSingleValueStatDesc(x: StatDescTypes): x is StatDesc {
return !isStatGroupDesc(x) && !isMultiValueStatDesc(x);
}
export type MultiValueStatDesc = {
label: string;
type: "multi";
} & ValueFormatOptions;
export function isMultiValueStatDesc(
x: StatDescTypes
): x is MultiValueStatDesc {
if (!("label" in x) || x.label === null || typeof x.label !== "string") {
return false;
}
return "type" in x && x.type === "multi";
}
export type StatsType<T extends StatGroupDesc> = {
[Property in keyof T["stats"]]: T["stats"][Property] extends StatGroupDesc
? StatsType<T["stats"][Property]>
: T["stats"][Property] extends MultiValueStatDesc
? Record<string, number>
: number;
};

View file

@ -1,14 +1,29 @@
import { ValueFormatOptions } from "../../../format/ValueFormatOptions"; import { ValueFormatOptions } from "../../../format/ValueFormatOptions";
type StatDesc = StatGroupDesc | SingleValueStatDesc | MultiValueStatDesc; export type StatDesc =
| StatGroupListDesc
| StatGroupDesc
| SingleValueStatDesc
| MultiValueStatDesc;
type NamedStatsDescriptors = {
[propName: string]: StatDesc;
};
/** /**
* Descripteur d'une Section de stat * Descripteur d'un groupe de stat
*/ */
export type StatGroupDesc = { export type StatGroupDesc = {
label: string; label: string;
desc?: string; desc?: string;
stats: { [propName: string]: StatDesc }; stats: NamedStatsDescriptors;
};
export type StatGroupListDesc = {
label: string;
desc?: string;
type: "group-list";
stats: NamedStatsDescriptors;
}; };
export function isStatGroupDesc(x: StatDesc): x is StatGroupDesc { export function isStatGroupDesc(x: StatDesc): x is StatGroupDesc {
@ -19,6 +34,14 @@ export function isStatGroupDesc(x: StatDesc): x is StatGroupDesc {
return true; return true;
} }
export function isStatGroupListDesc(x: StatDesc): x is StatGroupListDesc {
if (!("stats" in x) || x.stats === null || typeof x.stats !== "object") {
return false;
}
return "type" in x && x.type === "group-list";
}
/** /**
* Descripteur d'une valeur unique * Descripteur d'une valeur unique
*/ */
@ -44,10 +67,21 @@ export function isMultiValueStatDesc(x: StatDesc): x is MultiValueStatDesc {
export type StatsData = number | { value: number; relatedPageIds: string[] }; export type StatsData = number | { value: number; relatedPageIds: string[] };
export type StatsType<T extends StatGroupDesc> = { export type StatType<T extends StatDesc> = T extends StatGroupListDesc
[Property in keyof T["stats"]]: T["stats"][Property] extends StatGroupDesc ? Record<
? StatsType<T["stats"][Property]> string,
: T["stats"][Property] extends MultiValueStatDesc {
[Property in keyof T["stats"]]: StatType<T["stats"][Property]>;
}
>
: T extends StatGroupDesc
? {
[Property in keyof T["stats"]]: StatType<T["stats"][Property]>;
}
: T extends MultiValueStatDesc
? Record<string, StatsData> ? Record<string, StatsData>
: StatsData; : StatsData;
export type NamedStatsType<T extends NamedStatsDescriptors> = {
[Property in keyof T]: StatType<T[Property]>;
}; };

View file

@ -1,4 +1,4 @@
import { StatsType } from "../desc/StatsDesc"; import { StatType } from "../desc/StatsDesc";
export const statsGeneralesDesc = { export const statsGeneralesDesc = {
label: "Stats Générales", label: "Stats Générales",
@ -56,4 +56,4 @@ export const statsGeneralesDesc = {
}, },
} as const; } as const;
export type StatsGenerales = StatsType<typeof statsGeneralesDesc>; export type StatsGenerales = StatType<typeof statsGeneralesDesc>;

View file

@ -1,4 +1,4 @@
import { StatsType } from "../desc/StatsDesc"; import { StatType } from "../desc/StatsDesc";
export const statsTribunalCorrectionnelDesc = { export const statsTribunalCorrectionnelDesc = {
nbFamillesConvoquees: { nbFamillesConvoquees: {
@ -181,7 +181,33 @@ export const statsPenalesDesc = {
}, },
}, },
}, },
parDepartements: {
label: "Par départements",
type: "group-list",
stats: {
nbFamilles: {
label: "Nb familles",
},
pourcentageMED: {
label: "% familles avec une Mise en demeure",
unit: "%",
},
pourcentageProcedurePenaleHorsGendarmerie: {
label:
"% familles avec une Procédure Pénale différent de Gendarmerie (Procureur, Tribunal...)",
unit: "%",
},
pourcentageTribunalCorrectionnel: {
label: "% familles avec un evt Tribunal correctionnel",
unit: "%",
},
pourcentageTribunalPolice: {
label: "% familles avec un evt Tribunal de Police",
unit: "%",
},
},
},
}, },
} as const; } as const;
export type StatsPenales = StatsType<typeof statsPenalesDesc>; export type StatsPenales = StatType<typeof statsPenalesDesc>;

View file

@ -22,6 +22,9 @@ import {
FamilleAvecInfosProceduresPenales as FamilleAvecInfoProceduresPenales, FamilleAvecInfosProceduresPenales as FamilleAvecInfoProceduresPenales,
} from "./computeFamilleAvecInfosProceduresPenales"; } from "./computeFamilleAvecInfosProceduresPenales";
import { computeIntervalMedGendarmerieOuProcureur } from "./intervals/computeIntervalMedGendarmerieOuProcureur"; import { computeIntervalMedGendarmerieOuProcureur } from "./intervals/computeIntervalMedGendarmerieOuProcureur";
import { groupBy } from "lodash";
import { percent } from "../../../utils/math/percent";
import { isEvtTypeProcedurePenaleHorsGendarmerie } from "../../../data/TypeEvenementsPenal";
export type FamilleAvecInfoTribunaux = Famille & { export type FamilleAvecInfoTribunaux = Famille & {
infoTribunaux: InfoTribunalCorrectionnel[]; infoTribunaux: InfoTribunalCorrectionnel[];
@ -64,6 +67,12 @@ export function computeStatsPenales(familles: Famille[]): StatsPenales {
}; };
}); });
const famillesParDepartement = groupBy(
famillesResistantesOuEx,
(f) => f.Departement
);
const departements = Object.keys(famillesParDepartement).sort();
const famillesAvecInfoProceduresPenales: FamilleAvecInfoProceduresPenales[] = const famillesAvecInfoProceduresPenales: FamilleAvecInfoProceduresPenales[] =
famillesResistantesOuEx.map(computeFamilleAvecInfosProceduresPenales); famillesResistantesOuEx.map(computeFamilleAvecInfosProceduresPenales);
@ -206,10 +215,51 @@ export function computeStatsPenales(familles: Famille[]): StatsPenales {
), ),
}, },
}, },
parDepartements: Object.fromEntries(
departements.map((d) => [
d,
computeStatDepartement(famillesParDepartement[d]),
])
),
}; };
return statsPenales; return statsPenales;
} }
function computeStatDepartement(
famillesDepartements: Famille[]
): StatsPenales["parDepartements"][string] {
return {
nbFamilles: nbFamillesAvecPagesLiees(famillesDepartements),
pourcentageMED: percent(
filterFamillesWithOneOfEvenementsOfType(
famillesDepartements,
"Mise en demeure de scolarisation"
).length,
famillesDepartements.length
),
pourcentageProcedurePenaleHorsGendarmerie: percent(
filterFamillesWithOneOfEvenements(famillesDepartements, (e) =>
isEvtTypeProcedurePenaleHorsGendarmerie(e.Type)
).length,
famillesDepartements.length
),
pourcentageTribunalCorrectionnel: percent(
filterFamillesWithOneOfEvenements(
famillesDepartements,
(e) => e.Type === "Tribunal correctionnel"
).length,
famillesDepartements.length
),
pourcentageTribunalPolice: percent(
filterFamillesWithOneOfEvenements(
famillesDepartements,
(e) => e.Type === "Tribunal de police judiciaire"
).length,
famillesDepartements.length
),
};
}
function computeCrpc( function computeCrpc(
famillesResistantesOuEx: Famille[] famillesResistantesOuEx: Famille[]
): StatsPenales["procureur"]["crpc"] { ): StatsPenales["procureur"]["crpc"] {

View file

@ -1,4 +1,4 @@
import { StatsType } from "../desc/StatsDesc"; import { StatType } from "../desc/StatsDesc";
export const statsSocialesDesc = { export const statsSocialesDesc = {
label: "Stats Sociales", label: "Stats Sociales",
@ -72,4 +72,4 @@ export const statsSocialesDesc = {
}, },
} as const; } as const;
export type StatsSociales = StatsType<typeof statsSocialesDesc>; export type StatsSociales = StatType<typeof statsSocialesDesc>;