diff --git a/.gitignore b/.gitignore index 1d75daf..12063ec 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,4 @@ dist test-coverage node_modules -el-stats-par-anciennete.json -el-stats.json +el-stats*.json diff --git a/src/data/EvenementFamille.ts b/src/data/EvenementFamille.ts index a4f414e..3f11d36 100644 --- a/src/data/EvenementFamille.ts +++ b/src/data/EvenementFamille.ts @@ -90,6 +90,7 @@ export function isEvenementInPeriod( export function isEvenementBefore(evt: EvenementFamille, date: Date): boolean { if (evt.Date === null) { + // Assume lack of date are oldest events return true; } return evt.Date < date; @@ -101,3 +102,18 @@ export function isValidEvenementFamille(str: string | null): boolean { Object.prototype.hasOwnProperty.call(categorieEvenement, str) ); } +export function isGendarmerie(e: EvenementFamille): boolean { + return ( + e.Type === "Audition gendarmerie / police" || + e.Type === "Gendarmerie/Forces de l'ordre" || + e.Type === "Récidive gendarmerie" || + e.Type === "Passage police municipale" + ); +} +export function isEvtProcureur(e: EvenementFamille): boolean { + return ( + e.Type === "Audition procureur" || + e.Type === "Audience CRPC" || + e.Type === "Convocation CRPC" + ); +} diff --git a/src/data/Famille.ts b/src/data/Famille.ts index d27b72e..838a46b 100644 --- a/src/data/Famille.ts +++ b/src/data/Famille.ts @@ -13,6 +13,7 @@ export type Famille = { Integration: Date | null; ContexteEntree: ContexteEntreeDC; Sortie: Date | null; + // sorted by date asc Evenements: EvenementFamille[]; }; diff --git a/src/format/EvolFormatOptions.ts b/src/format/EvolFormatOptions.ts new file mode 100644 index 0000000..82ab2dd --- /dev/null +++ b/src/format/EvolFormatOptions.ts @@ -0,0 +1,4 @@ +export type EvolFormatOptions = { + evolMaxFractioDigits?: number; + evolPctMaxFractioDigits?: number; +}; diff --git a/src/format/ValueFormatOptions.ts b/src/format/ValueFormatOptions.ts new file mode 100644 index 0000000..42e223d --- /dev/null +++ b/src/format/ValueFormatOptions.ts @@ -0,0 +1,4 @@ +export type ValueFormatOptions = { + unit?: string; + valueMaxFractioDigits?: number; +}; diff --git a/src/notion/publish/format/formatValue.test.ts b/src/format/formatValue.test.ts similarity index 69% rename from src/notion/publish/format/formatValue.test.ts rename to src/format/formatValue.test.ts index f86f43e..2a2248c 100644 --- a/src/notion/publish/format/formatValue.test.ts +++ b/src/format/formatValue.test.ts @@ -3,19 +3,17 @@ import { formatValue } from "./formatValue"; describe("formatValue", () => { test("format with default options", () => { - expect(formatValue(42.42, { notionPropName: "whatever" })).toBe("42,4"); - expect(formatValue(42, { notionPropName: "whatever" })).toBe("42"); + expect(formatValue(42.42, {})).toBe("42,4"); + expect(formatValue(42, {})).toBe("42"); }); test("format with valueMaxFractioDigits: 2", () => { expect( formatValue(42.4242, { - notionPropName: "whatever", valueMaxFractioDigits: 2, }) ).toBe("42,42"); expect( formatValue(42, { - notionPropName: "whatever", valueMaxFractioDigits: 2, }) ).toBe("42"); @@ -24,7 +22,6 @@ describe("formatValue", () => { test("format with unit", () => { expect( formatValue(42, { - notionPropName: "whatever", unit: "%", }) ).toBe("42%"); diff --git a/src/notion/publish/format/formatValue.ts b/src/format/formatValue.ts similarity index 71% rename from src/notion/publish/format/formatValue.ts rename to src/format/formatValue.ts index 6202079..4a3e36f 100644 --- a/src/notion/publish/format/formatValue.ts +++ b/src/format/formatValue.ts @@ -1,6 +1,6 @@ -import { StatPublishOptions } from "../../statPublishOptions"; +import { ValueFormatOptions } from "./ValueFormatOptions"; -export function formatValue(value: number, publishOptions: StatPublishOptions) { +export function formatValue(value: number, publishOptions: ValueFormatOptions) { const valueStr = value.toLocaleString("fr-FR", { useGrouping: false, maximumFractionDigits: diff --git a/src/notion/publish/format/formatValueWithEvol.ts b/src/format/formatValueWithEvol.ts similarity index 86% rename from src/notion/publish/format/formatValueWithEvol.ts rename to src/format/formatValueWithEvol.ts index 4bedd98..48ec426 100644 --- a/src/notion/publish/format/formatValueWithEvol.ts +++ b/src/format/formatValueWithEvol.ts @@ -1,5 +1,5 @@ -import { ValueWithEvol } from "../../../statistiques/ELStats"; -import { StatPublishOptions } from "../../statPublishOptions"; +import { StatPublishOptions } from "../notion/publish/v1/statPublishOptions"; +import { ValueWithEvol } from "../statistiques/v1/ELStats"; import { formatValue } from "./formatValue"; export function formatValueWithEvol( diff --git a/src/index.ts b/src/index.ts index e1da1dd..b1527c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,12 @@ import { Client } from "@notionhq/client"; import { writeFileSync } from "fs"; import { checkDataConsistency } from "./data/checkDataConsistency"; import { fetchFamiliesWithEventsFromNotion } from "./notion/fetch/fetchFamiliesWithEventsFromNotion"; -import { publishStatisticsToNotion } from "./notion/publish/publishStatisticsToNotion"; -import { computeELStats } from "./statistiques/computeELStats"; -import { computeStatsParAnciennete } from "./statistiques/computeEvenementsParAnciennete"; +import { publishStatisticsToNotion } from "./notion/publish/v1/publishStatisticsToNotion"; +import { publishStatsToPage } from "./notion/publish/v2/publishStatsToPage"; +import { computeELStats } from "./statistiques/v1/computeELStats"; +import { computeStatsParAnciennete } from "./statistiques/v1/computeEvenementsParAnciennete"; +import { computeStatsPenales } from "./statistiques/v2/penales/computeStatsPenales"; +import { statsPenalesDesc } from "./statistiques/v2/penales/StatsPenales"; type ProcessOptions = { dryRun: boolean; @@ -65,13 +68,26 @@ function buildProcessOptions(): ProcessOptions { "./el-stats-par-anciennete.json", JSON.stringify(statsParAnciennete, null, " ") ); + + const statsV2 = computeStatsPenales(familles); + if (options.dryRun) { console.log( - "Dry run => Skip Publishing. Stats are dumped in file el-stats.json" + "Dry run => Skip Publishing. Stats are dumped in file el-stats-xxx.json" ); - writeFileSync("./el-stats.json", JSON.stringify(elStats, null, " ")); + writeFileSync("./el-stats-v1.json", JSON.stringify(elStats, null, " ")); + + writeFileSync("./el-stats-v2.json", JSON.stringify(statsV2, null, " ")); } else { console.log("Publishing statistics..."); - publishStatisticsToNotion(elStats, currentDate, notionClient); + await publishStatisticsToNotion(elStats, currentDate, notionClient); + + await publishStatsToPage( + notionClient, + "969eac5c-a4eb-49d4-b4ad-c341c9c4c785", + + statsPenalesDesc, + statsV2 + ); } })(); diff --git a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts index efb5043..2b0167f 100644 --- a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts +++ b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts @@ -22,6 +22,11 @@ export async function fetchFamiliesWithEventsFromNotion( const eventPages = ( await queryAllDbResults(notionClient, { database_id: familEventsDbId, + sorts: [ + {property: "Date", + direction: "ascending" + } + ] }) ).filter(isFullPage); const familyPages = ( diff --git a/src/notion/publish/publishPeriodStats.ts b/src/notion/publish/v1/publishPeriodStats.ts similarity index 90% rename from src/notion/publish/publishPeriodStats.ts rename to src/notion/publish/v1/publishPeriodStats.ts index f9ff7cb..4ce76ca 100644 --- a/src/notion/publish/publishPeriodStats.ts +++ b/src/notion/publish/v1/publishPeriodStats.ts @@ -3,17 +3,17 @@ import { PageObjectResponse, UpdateDatabaseParameters, } from "@notionhq/client/build/src/api-endpoints"; +import { formatValueWithEvol } from "../../../format/formatValueWithEvol"; 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"; +} from "../../../statistiques/v1/ELStats"; +import { titlePropertyToText } from "../../utils/properties/titlePropertyToText"; +import { queryAllDbResults } from "../../utils/queryAllDbResults"; +import { removeBlocks } from "../../utils/removeBlocks"; +import { CreatePageProperties } from "../../utils/types/CreatePageProperties"; +import { StatPublishOptions, statPublishOptions } from "./statPublishOptions"; const periodeDbPropertyName = "Période"; diff --git a/src/notion/publish/publishStatisticsToNotion.ts b/src/notion/publish/v1/publishStatisticsToNotion.ts similarity index 88% rename from src/notion/publish/publishStatisticsToNotion.ts rename to src/notion/publish/v1/publishStatisticsToNotion.ts index 45120dc..25b2507 100644 --- a/src/notion/publish/publishStatisticsToNotion.ts +++ b/src/notion/publish/v1/publishStatisticsToNotion.ts @@ -1,7 +1,7 @@ import { Client, isFullBlock } from "@notionhq/client"; -import { ELStats } from "../../statistiques/ELStats"; -import { listAllChildrenBlocks } from "../utils/listAllChildrenBlocks"; -import { richTextToPlainText } from "../utils/text/richTextToPlainText"; +import { ELStats } from "../../../statistiques/v1/ELStats"; +import { listAllChildrenBlocks } from "../../utils/listAllChildrenBlocks"; +import { richTextToPlainText } from "../../utils/text/richTextToPlainText"; import { publishPeriodStats } from "./publishPeriodStats"; import { publishStatsActuelles } from "./publishStatsActuelles"; diff --git a/src/notion/publish/publishStatsActuelles.ts b/src/notion/publish/v1/publishStatsActuelles.ts similarity index 84% rename from src/notion/publish/publishStatsActuelles.ts rename to src/notion/publish/v1/publishStatsActuelles.ts index 0c059de..35b6a22 100644 --- a/src/notion/publish/publishStatsActuelles.ts +++ b/src/notion/publish/v1/publishStatsActuelles.ts @@ -1,12 +1,12 @@ import { Client, isFullBlock } from "@notionhq/client"; import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints"; -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"; -import { formatValue } from "./format/formatValue"; +import { formatValue } from "../../../format/formatValue"; +import { ELStatsAtDate } from "../../../statistiques/v1/ELStats"; +import { listAllChildrenBlocks } from "../../utils/listAllChildrenBlocks"; +import { removeBlocks } from "../../utils/removeBlocks"; +import { richTextToPlainText } from "../../utils/text/richTextToPlainText"; import { currentStatsHeading, statsPageId } from "./publishStatisticsToNotion"; +import { StatPublishOptions, statPublishOptions } from "./statPublishOptions"; export async function publishStatsActuelles( notionClient: Client, diff --git a/src/notion/statPublishOptions.ts b/src/notion/publish/v1/statPublishOptions.ts similarity index 95% rename from src/notion/statPublishOptions.ts rename to src/notion/publish/v1/statPublishOptions.ts index eae4347..e225f62 100644 --- a/src/notion/statPublishOptions.ts +++ b/src/notion/publish/v1/statPublishOptions.ts @@ -1,4 +1,6 @@ -import { AllStatsPropNames } from "../statistiques/ELStats"; +import { EvolFormatOptions } from "../../../format/EvolFormatOptions"; +import { ValueFormatOptions } from "../../../format/ValueFormatOptions"; +import { AllStatsPropNames } from "../../../statistiques/v1/ELStats"; export function statPublishOptions( statJsPropName: AllStatsPropNames @@ -185,8 +187,5 @@ const statPropsPublishOptions: { }; export type StatPublishOptions = { notionPropName: string; - unit?: string; - valueMaxFractioDigits?: number; - evolMaxFractioDigits?: number; - evolPctMaxFractioDigits?: number; -}; +} & ValueFormatOptions & + EvolFormatOptions; diff --git a/src/notion/publish/v2/publishStatsToPage.ts b/src/notion/publish/v2/publishStatsToPage.ts new file mode 100644 index 0000000..5ecbb82 --- /dev/null +++ b/src/notion/publish/v2/publishStatsToPage.ts @@ -0,0 +1,97 @@ +import { Client, isFullBlock } from "@notionhq/client"; +import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints"; +import { formatValue } from "../../../format/formatValue"; +import { + isStatGroupDesc, + StatDesc, + StatGroupDesc, + StatsType, +} from "../../../statistiques/v2/StatsDesc"; +import { listAllChildrenBlocks } from "../../utils/listAllChildrenBlocks"; +import { removeBlocks } from "../../utils/removeBlocks"; + +export async function publishStatsToPage( + notionClient: Client, + statsPageId: string, + descriptor: D, + stats: StatsType +) { + const childrenBlocks = ( + await listAllChildrenBlocks(notionClient, { + block_id: statsPageId, + }) + ).filter(isFullBlock); + const blocksIdsToRemove = childrenBlocks.map((b) => b.id); + await removeBlocks(notionClient, blocksIdsToRemove); + + const newBlocks = createStatGroupChildrenListItemBlock(descriptor, stats); + + await notionClient.blocks.children.append({ + block_id: statsPageId, + children: [...newBlocks], + }); +} + +function createStatGroupListItemBlock( + descriptor: D, + stats: StatsType +): BulletedListItemBlockObjectRequest { + return { + bulleted_list_item: { + rich_text: [ + { + text: { + content: descriptor.label, + }, + }, + ], + children: createStatGroupChildrenListItemBlock( + descriptor, + stats + ) as BulletedListItemChildren, + }, + }; +} + +type BulletedListItemBlockObjectRequest = Extract< + BlockObjectRequest, + { bulleted_list_item: object } +>; + +type BulletedListItemChildren = + BulletedListItemBlockObjectRequest["bulleted_list_item"]["children"]; + +function createStatGroupChildrenListItemBlock( + descriptor: D, + stats: StatsType +): BulletedListItemBlockObjectRequest[] { + return Object.keys(descriptor.stats).map((statName) => { + const childStatDesc = descriptor.stats[statName]; + const childStatValue = stats[statName]; + + return isStatGroupDesc(childStatDesc) + ? createStatGroupListItemBlock( + childStatDesc, + childStatValue as StatsType + ) + : createStatListItemBlock(childStatDesc, childStatValue as number); + }); +} + +function createStatListItemBlock( + descriptor: StatDesc, + statValue: number +): BulletedListItemBlockObjectRequest { + return { + bulleted_list_item: { + rich_text: [ + { + text: { + content: + descriptor.label + ": " + formatValue(statValue, descriptor), + }, + }, + ], + }, + }; +} diff --git a/src/notion/publish/v2/publishStatsV2.ts b/src/notion/publish/v2/publishStatsV2.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/statistiques/ELStats.ts b/src/statistiques/v1/ELStats.ts similarity index 100% rename from src/statistiques/ELStats.ts rename to src/statistiques/v1/ELStats.ts diff --git a/src/statistiques/computeELPeriodStats.ts b/src/statistiques/v1/computeELPeriodStats.ts similarity index 93% rename from src/statistiques/computeELPeriodStats.ts rename to src/statistiques/v1/computeELPeriodStats.ts index 38940a3..95e9d75 100644 --- a/src/statistiques/computeELPeriodStats.ts +++ b/src/statistiques/v1/computeELPeriodStats.ts @@ -1,6 +1,6 @@ -import { Famille } from "../data/Famille"; -import { IdentifiedPeriod } from "../period/IdentifiedPeriod"; -import { Period } from "../period/Period"; +import { Famille } from "../../data/Famille"; +import { IdentifiedPeriod } from "../../period/IdentifiedPeriod"; +import { Period } from "../../period/Period"; import { ELStatsOverPeriod, ELStatsPeriod, diff --git a/src/statistiques/computeELStats.ts b/src/statistiques/v1/computeELStats.ts similarity index 93% rename from src/statistiques/computeELStats.ts rename to src/statistiques/v1/computeELStats.ts index 1d6b7b4..78325e4 100644 --- a/src/statistiques/computeELStats.ts +++ b/src/statistiques/v1/computeELStats.ts @@ -1,7 +1,7 @@ -import { Famille } from "../data/Famille"; -import { ELStats } from "./ELStats"; +import { Famille } from "../../data/Famille"; import { computeELPeriodStats } from "./computeELPeriodStats"; import { computeELStatsAtDate } from "./computeELStatsAtDate"; +import { ELStats } from "./ELStats"; import { generateELMonths } from "./generateELMonths"; import { generateELYears } from "./generateELYears"; diff --git a/src/statistiques/computeELStatsAtDate.ts b/src/statistiques/v1/computeELStatsAtDate.ts similarity index 97% rename from src/statistiques/computeELStatsAtDate.ts rename to src/statistiques/v1/computeELStatsAtDate.ts index a83c464..d249307 100644 --- a/src/statistiques/computeELStatsAtDate.ts +++ b/src/statistiques/v1/computeELStatsAtDate.ts @@ -4,17 +4,17 @@ import { isEvenementBefore, isProcedureCivile, isProcedurePenale, -} from "../data/EvenementFamille"; +} 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"; +} 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"; diff --git a/src/statistiques/computeELStatsOverPeriod.ts b/src/statistiques/v1/computeELStatsOverPeriod.ts similarity index 89% rename from src/statistiques/computeELStatsOverPeriod.ts rename to src/statistiques/v1/computeELStatsOverPeriod.ts index ed040e4..ce5d1a8 100644 --- a/src/statistiques/computeELStatsOverPeriod.ts +++ b/src/statistiques/v1/computeELStatsOverPeriod.ts @@ -1,14 +1,14 @@ import { isEvenementInPeriod, isProcedurePenale, -} from "../data/EvenementFamille"; +} from "../../data/EvenementFamille"; import { Famille, isResistantOverPeriod, periodOfResistance, -} from "../data/Famille"; -import { Period } from "../period/Period"; -import { isPeriodContaining } from "../period/isPeriodContaining"; +} from "../../data/Famille"; +import { Period } from "../../period/Period"; +import { isPeriodContaining } from "../../period/isPeriodContaining"; import { ELStatsOverPeriod } from "./ELStats"; import { computePourcentageEntreeApresMiseEnDemeure } from "./computePourcentageEntreeApresMiseEnDemeure"; diff --git a/src/statistiques/computeEvenementsParAnciennete.ts b/src/statistiques/v1/computeEvenementsParAnciennete.ts similarity index 89% rename from src/statistiques/computeEvenementsParAnciennete.ts rename to src/statistiques/v1/computeEvenementsParAnciennete.ts index 4cb1dfe..e851630 100644 --- a/src/statistiques/computeEvenementsParAnciennete.ts +++ b/src/statistiques/v1/computeEvenementsParAnciennete.ts @@ -1,5 +1,8 @@ -import { isProcedureCivile, isProcedurePenale } from "../data/EvenementFamille"; -import { Famille } from "../data/Famille"; +import { + isProcedureCivile, + isProcedurePenale, +} from "../../data/EvenementFamille"; +import { Famille } from "../../data/Famille"; import { computeFamillesWithEventsConditionInEarlyPeriod } from "./computeFamilleWithEventAfterDurationOfDC"; export function computeStatsParAnciennete(familles: Famille[]) { diff --git a/src/statistiques/computeFamilleWithEventAfterDurationOfDC.ts b/src/statistiques/v1/computeFamilleWithEventAfterDurationOfDC.ts similarity index 90% rename from src/statistiques/computeFamilleWithEventAfterDurationOfDC.ts rename to src/statistiques/v1/computeFamilleWithEventAfterDurationOfDC.ts index 9f09a62..95e0bf7 100644 --- a/src/statistiques/computeFamilleWithEventAfterDurationOfDC.ts +++ b/src/statistiques/v1/computeFamilleWithEventAfterDurationOfDC.ts @@ -1,7 +1,10 @@ import { addMonths } from "date-fns"; -import { EvenementFamille, isEvenementBefore } from "../data/EvenementFamille"; -import { Famille } from "../data/Famille"; -import { percent } from "../utils/math/percent"; +import { + EvenementFamille, + isEvenementBefore, +} from "../../data/EvenementFamille"; +import { Famille } from "../../data/Famille"; +import { percent } from "../../utils/math/percent"; type FamillesWithEventsConditionInEarlyPeriod = { [name: string]: { diff --git a/src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts b/src/statistiques/v1/computePourcentageEntreeApresMiseEnDemeure.ts similarity index 81% rename from src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts rename to src/statistiques/v1/computePourcentageEntreeApresMiseEnDemeure.ts index 3b19a51..f5f2516 100644 --- a/src/statistiques/computePourcentageEntreeApresMiseEnDemeure.ts +++ b/src/statistiques/v1/computePourcentageEntreeApresMiseEnDemeure.ts @@ -1,5 +1,5 @@ -import { Famille } from "../data/Famille"; -import { percent } from "../utils/math/percent"; +import { Famille } from "../../data/Famille"; +import { percent } from "../../utils/math/percent"; export function computePourcentageEntreeApresMiseEnDemeure( familles: Famille[] diff --git a/src/statistiques/generateELMonths.ts b/src/statistiques/v1/generateELMonths.ts similarity index 85% rename from src/statistiques/generateELMonths.ts rename to src/statistiques/v1/generateELMonths.ts index 8cfdff5..6ac06f7 100644 --- a/src/statistiques/generateELMonths.ts +++ b/src/statistiques/v1/generateELMonths.ts @@ -1,6 +1,6 @@ import { getMonth, getYear, setMonth, setYear } from "date-fns"; -import { IdentifiedPeriod } from "../period/IdentifiedPeriod"; -import { generateConsecutiveIdentifiedPeriods } from "../period/generateConsecutiveIdentifiedPeriods"; +import { IdentifiedPeriod } from "../../period/IdentifiedPeriod"; +import { generateConsecutiveIdentifiedPeriods } from "../../period/generateConsecutiveIdentifiedPeriods"; export function generateELMonths(): IdentifiedPeriod[] { const months = generateConsecutiveIdentifiedPeriods({ diff --git a/src/statistiques/generateELYears.ts b/src/statistiques/v1/generateELYears.ts similarity index 76% rename from src/statistiques/generateELYears.ts rename to src/statistiques/v1/generateELYears.ts index 259a703..a86dc55 100644 --- a/src/statistiques/generateELYears.ts +++ b/src/statistiques/v1/generateELYears.ts @@ -1,6 +1,6 @@ import { addYears } from "date-fns/fp"; -import { IdentifiedPeriod } from "../period/IdentifiedPeriod"; -import { generateConsecutiveIdentifiedPeriods } from "../period/generateConsecutiveIdentifiedPeriods"; +import { IdentifiedPeriod } from "../../period/IdentifiedPeriod"; +import { generateConsecutiveIdentifiedPeriods } from "../../period/generateConsecutiveIdentifiedPeriods"; export function generateELYears(): IdentifiedPeriod[] { return generateConsecutiveIdentifiedPeriods({ diff --git a/src/statistiques/v2/ELStatsV2.ts b/src/statistiques/v2/ELStatsV2.ts new file mode 100644 index 0000000..f652e32 --- /dev/null +++ b/src/statistiques/v2/ELStatsV2.ts @@ -0,0 +1,11 @@ +import { statsPenalesDesc } from "./penales/StatsPenales"; +import { StatsType } from "./StatsDesc"; + +export const eLStatsV2Desc = { + label: "Stats v2", + stats: { + penales: statsPenalesDesc, + }, +}; + +export type ELStatsV2 = StatsType; diff --git a/src/statistiques/v2/StatsDesc.ts b/src/statistiques/v2/StatsDesc.ts new file mode 100644 index 0000000..72da807 --- /dev/null +++ b/src/statistiques/v2/StatsDesc.ts @@ -0,0 +1,32 @@ +import { ValueFormatOptions } from "../../format/ValueFormatOptions"; + +export type StatGroupDesc = { + label: string; + desc?: string; + stats: { [propName: string]: StatDesc | StatGroupDesc }; +}; + +export type StatDesc = { + label: string; +} & ValueFormatOptions; + +export function isStatGroupDesc(x: unknown): x is StatGroupDesc { + if (typeof x !== "object" || Array.isArray(x) || x === null) { + return false; + } + if (!("label" in x) || x.label === null || typeof x.label !== "string") { + return false; + } + + if (!("stats" in x) || x.stats === null || typeof x.stats !== "object") { + return false; + } + + return true; +} + +export type StatsType = { + [Property in keyof T["stats"]]: T["stats"][Property] extends StatGroupDesc + ? StatsType + : number; +}; diff --git a/src/statistiques/v2/filterFamillesWithOneOfEvenements.ts b/src/statistiques/v2/filterFamillesWithOneOfEvenements.ts new file mode 100644 index 0000000..37ec9b1 --- /dev/null +++ b/src/statistiques/v2/filterFamillesWithOneOfEvenements.ts @@ -0,0 +1,11 @@ +import { EvenementFamille } from "../../data/EvenementFamille"; +import { Famille } from "../../data/Famille"; + +export function filterFamillesWithOneOfEvenements( + familles: Famille[], + evenementtPredicated: (evt: EvenementFamille) => boolean +): Famille[] { + return familles.filter( + (f) => f.Evenements.find((e) => evenementtPredicated(e)) !== undefined + ); +} diff --git a/src/statistiques/v2/filterFamillesWithOneOfEvenementsOfType.ts b/src/statistiques/v2/filterFamillesWithOneOfEvenementsOfType.ts new file mode 100644 index 0000000..fee5f65 --- /dev/null +++ b/src/statistiques/v2/filterFamillesWithOneOfEvenementsOfType.ts @@ -0,0 +1,13 @@ +import { Famille } from "../../data/Famille"; +import { TypeEvenement } from "../../data/TypeEvenement"; +import { filterFamillesWithOneOfEvenements } from "./filterFamillesWithOneOfEvenements"; + +export function filterFamillesWithOneOfEvenementsOfType( + familles: Famille[], + eventType: TypeEvenement +): Famille[] { + return filterFamillesWithOneOfEvenements( + familles, + (e) => e.Type === eventType + ); +} diff --git a/src/statistiques/v2/penales/StatsPenales.ts b/src/statistiques/v2/penales/StatsPenales.ts new file mode 100644 index 0000000..d29860f --- /dev/null +++ b/src/statistiques/v2/penales/StatsPenales.ts @@ -0,0 +1,66 @@ +import { StatsType } from "../StatsDesc"; + +export const statsPenalesDesc = { + label: "Stats Pénales", + stats: { + nbFamillesMisesEnDemeure: { + label: "Nb familles mises en demeure", + }, + nbFamillesAvecGendarmerie: { + label: "Nb familles avec un evenement lié à la Gendarmerie", + }, + compositionPenales: { + label: "Compositions Pénales", + stats: { + nbFamilles: { + label: "Nb familles concernées", + }, + acceptees: { + label: "Nb familles ayant acceptées", + }, + refusees: { + label: "Nb familles ayant refusées", + }, + }, + }, + crpc: { + label: "CRPC", + stats: { + nbFamilles: { + label: "Nb familles concernées", + }, + acceptees: { + label: "Nb familles ayant acceptées", + }, + refusees: { + label: "Nb familles ayant refusées", + }, + }, + }, + tribunalCorrectionnel: { + label: "Tribunal Correctionnel", + stats: { + nbFamillesPassees: { + label: "Nb familles passées", + }, + nbFamillesProgrammees: { + label: "Nb familles programmées", + }, + nbFamillesRecidive: { + label: "Nb familles recidive", + }, + }, + }, + + intervalGendarmerieProcureur: { + label: "Délai moyen entre Gendarmerie et Procureur", + unit: " jours", + }, + intervalProcureurTribunalCorrectionnel: { + label: "Délai moyen entre Procureur et Tribunal Correctionnel", + unit: " jours", + }, + }, +} as const; + +export type StatsPenales = StatsType; diff --git a/src/statistiques/v2/penales/computeStatsPenales.ts b/src/statistiques/v2/penales/computeStatsPenales.ts new file mode 100644 index 0000000..b7d95b0 --- /dev/null +++ b/src/statistiques/v2/penales/computeStatsPenales.ts @@ -0,0 +1,159 @@ +import { differenceInDays } from "date-fns"; +import { + isCompositionPenale, + isCRPC, + isEvenementBefore, + isEvtProcureur, + isGendarmerie, +} from "../../../data/EvenementFamille"; +import { Famille } from "../../../data/Famille"; +import { average } from "../../../utils/math/average"; +import { filterFamillesWithOneOfEvenements } from "../filterFamillesWithOneOfEvenements"; +import { filterFamillesWithOneOfEvenementsOfType } from "../filterFamillesWithOneOfEvenementsOfType"; +import { StatsPenales } from "./StatsPenales"; + +export function computeStatsPenales(familles: Famille[]): StatsPenales { + const famillesMisesEnDemeure = filterFamillesWithOneOfEvenementsOfType( + familles, + "Mise en demeure de scolarisation" + ); + const famillesAvecGendarmerie = filterFamillesWithOneOfEvenements( + familles, + isGendarmerie + ); + const statsPenales: StatsPenales = { + nbFamillesMisesEnDemeure: famillesMisesEnDemeure.length, + nbFamillesAvecGendarmerie: famillesAvecGendarmerie.length, + compositionPenales: computeCompositionPenales(familles), + crpc: computeCrpc(familles), + tribunalCorrectionnel: computeTribunalCorrectionnel(familles), + intervalGendarmerieProcureur: computeIntervalGendarmerieProcureur(familles), + intervalProcureurTribunalCorrectionnel: + computeIntervalProcureurTribunalCorrectionnel(familles), + }; + return statsPenales; +} + +function computeCrpc(familles: Famille[]): StatsPenales["crpc"] { + const famillesConcernees = filterFamillesWithOneOfEvenements(familles, (e) => + isCRPC(e) + ); + const acceptees = filterFamillesWithOneOfEvenementsOfType( + familles, + "Acceptation CRPC" + ); + const refusees = filterFamillesWithOneOfEvenementsOfType( + familles, + "Refus CRPC" + ); + + return { + nbFamilles: famillesConcernees.length, + acceptees: acceptees.length, + refusees: refusees.length, + }; +} + +function computeCompositionPenales( + familles: Famille[] +): StatsPenales["compositionPenales"] { + const famillesConcernees = filterFamillesWithOneOfEvenements(familles, (e) => + isCompositionPenale(e) + ); + const acceptees = filterFamillesWithOneOfEvenementsOfType( + familles, + "Composition pénale acceptée" + ); + const refusees = filterFamillesWithOneOfEvenementsOfType( + familles, + "Composition pénale refusée" + ); + + return { + nbFamilles: famillesConcernees.length, + acceptees: acceptees.length, + refusees: refusees.length, + }; +} + +function computeTribunalCorrectionnel( + familles: Famille[] +): StatsPenales["tribunalCorrectionnel"] { + const now = new Date(); + const famillesPassees = filterFamillesWithOneOfEvenements( + familles, + (e) => e.Type === "Tribunal correctionnel" && isEvenementBefore(e, now) + ); + const famillesProgrammees = filterFamillesWithOneOfEvenements( + familles, + (e) => e.Type === "Tribunal correctionnel" && !isEvenementBefore(e, now) + ); + + const famillesRecidiveTribunal = familles.filter((f) => { + return ( + f.Evenements.filter((e) => e.Type === "Tribunal correctionnel").length > 1 + ); + }); + + return { + nbFamillesPassees: famillesPassees.length, + nbFamillesProgrammees: famillesProgrammees.length, + nbFamillesRecidive: famillesRecidiveTribunal.length, + }; +} + +function computeIntervalGendarmerieProcureur(familles: Famille[]): number { + const intervals = familles.flatMap((f) => { + const evtGendarmerie = f.Evenements.find((e) => isGendarmerie(e)); + const evtProcureur = f.Evenements.find((e) => isEvtProcureur(e)); + + // consider only intervals for families with both events date + if (!evtGendarmerie?.Date || !evtProcureur?.Date) { + return []; + } + + const intervalInDays = differenceInDays( + evtProcureur.Date, + evtGendarmerie.Date + ); + if (intervalInDays < 0) { + console.warn( + `IntervalGendarmerieProcureur < 0 for ${f.Titre} (${f.notionId})` + ); + return []; + } else { + return [intervalInDays]; + } + }); + return average(intervals); +} + +function computeIntervalProcureurTribunalCorrectionnel( + familles: Famille[] +): number { + const intervals = familles.flatMap((f) => { + const evtProcureur = f.Evenements.find((e) => isEvtProcureur(e)); + const evtTribunal = f.Evenements.find( + (e) => e.Type === "Tribunal correctionnel" + ); + + // consider only intervals for families with both events date + if (!evtProcureur?.Date || !evtTribunal?.Date) { + return []; + } + + const intervalInDays = differenceInDays( + evtTribunal.Date, + evtProcureur.Date + ); + if (intervalInDays < 0) { + console.warn( + `IntervalProcureurTribunalCorrectionnel < 0 for ${f.Titre} (${f.notionId})` + ); + return []; + } else { + return [intervalInDays]; + } + }); + return average(intervals); +}