From c82ae0833b56ecd3830c91d544abe2efbfc86e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Arod?= Date: Mon, 23 Jun 2025 09:28:57 +0200 Subject: [PATCH] feat: stats departementales tabulaire --- src/data/Departement.ts | 4 + src/index.ts | 22 +- src/notion/fetch/fetchContacts.ts | 40 +++ src/notion/fetch/fetchDepartements.ts | 26 ++ src/notion/fetch/fetchEvenements.ts | 53 ++++ .../fetchFamiliesWithEventsFromNotion.ts | 257 +----------------- src/notion/fetch/fetchFamilles.ts | 112 ++++++++ src/notion/fetch/fetchMissions.ts | 32 +++ .../publish/publishStatsDepartementales.ts | 119 ++++++++ .../departemental/computeStatDepartement.ts | 106 ++++++++ .../v2/penales/computeStatDepartement.ts | 42 +++ .../v2/penales/computeStatsPenales.ts | 38 +-- 12 files changed, 566 insertions(+), 285 deletions(-) create mode 100644 src/data/Departement.ts create mode 100644 src/notion/fetch/fetchContacts.ts create mode 100644 src/notion/fetch/fetchDepartements.ts create mode 100644 src/notion/fetch/fetchEvenements.ts create mode 100644 src/notion/fetch/fetchFamilles.ts create mode 100644 src/notion/fetch/fetchMissions.ts create mode 100644 src/notion/publish/publishStatsDepartementales.ts create mode 100644 src/statistiques/v2/departemental/computeStatDepartement.ts create mode 100644 src/statistiques/v2/penales/computeStatDepartement.ts diff --git a/src/data/Departement.ts b/src/data/Departement.ts new file mode 100644 index 0000000..b977517 --- /dev/null +++ b/src/data/Departement.ts @@ -0,0 +1,4 @@ +export type Departement = { + notionId: string; + name: string; +}; diff --git a/src/index.ts b/src/index.ts index 1fd2381..70a0b9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { Client } from "@notionhq/client"; -import { writeFileSync } from "fs"; +import { stat, Stats, writeFileSync } from "fs"; import { fetchFamiliesWithEventsFromNotion } from "./notion/fetch/fetchFamiliesWithEventsFromNotion"; import { publishStatsToPage } from "./notion/publish/v2/publishStatsToPage"; import { computeStatsPenales } from "./statistiques/v2/penales/computeStatsPenales"; @@ -19,6 +19,8 @@ import { nettoyerDonneesFamilles } from "./data/nettoyage/familles/preparerDonne import { statsAutresDesc } from "./statistiques/v2/autres/StatsAutres"; import { computeStatsAutres } from "./statistiques/v2/autres/computeStatsAutres"; import { updateUpdateDate as updateRootPageUpdateDate } from "./notion/publish/updateUpdateDate"; +import { computeStatsDepartementales } from "./statistiques/v2/departemental/computeStatDepartement"; +import { publishStatsDepartementales } from "./notion/publish/publishStatsDepartementales"; type ProcessOptions = { dryRun: boolean; @@ -51,12 +53,12 @@ function buildProcessOptions(): ProcessOptions { await checkDbSchemas(notionClient); console.log("Téléchargement des données..."); - const donneesFamillesBrutes = await fetchFamiliesWithEventsFromNotion( - notionClient - ); + const donneesNotion = await fetchFamiliesWithEventsFromNotion(notionClient); console.log("Nettoyage des données brutes..."); - const { familles, messages } = nettoyerDonneesFamilles(donneesFamillesBrutes); + const { familles, messages } = nettoyerDonneesFamilles( + donneesNotion.familles + ); const donneesBloquantes = messages.filter( (m) => m.severite === "Donnée incohérente bloquante" @@ -107,6 +109,10 @@ function buildProcessOptions(): ProcessOptions { const statsSociales = computeStatsSociales(familles); const statsAutres = computeStatsAutres(familles); const statsGeneralesMensuelles = computeStatsGeneralesMensuelles(familles); + const statsDepartementales = computeStatsDepartementales( + familles, + donneesNotion.departements + ); const mermaidDiagramStatsGeneralesMensuellesCode = mermaidDiagramStatsGeneralesMensuelles(statsGeneralesMensuelles); writeFileSync( @@ -131,6 +137,7 @@ function buildProcessOptions(): ProcessOptions { sociales: statsSociales, statsAutres: statsAutres, StatsGeneralesMensuelles: statsGeneralesMensuelles, + statsDepartementales: statsDepartementales, }, null, " " @@ -177,5 +184,10 @@ function buildProcessOptions(): ProcessOptions { statsAutresDesc, statsAutres ); + await publishStatsDepartementales( + notionClient, + "21a168be9f1980a7a61df867770483c3", + statsDepartementales + ); } })(); diff --git a/src/notion/fetch/fetchContacts.ts b/src/notion/fetch/fetchContacts.ts new file mode 100644 index 0000000..bbdc20d --- /dev/null +++ b/src/notion/fetch/fetchContacts.ts @@ -0,0 +1,40 @@ +import { Client, isFullPage } from "@notionhq/client"; +import { Contact } from "../../data/Contact"; +import { checkboxPropertyToBoolean } from "../utils/properties/checkboxPropertyToBoolean"; +import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId"; +import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; +import { queryAllDbResults } from "../utils/queryAllDbResults"; +import { contactsDbId } from "./dbIds"; + +export async function fetchContacts( + notionClient: Client, + cacheConfig: boolean | { ttl: number }, + missions: Readonly<{ + notionId: string; + Nom: string; + Equipe: string; + ContactsNotionIds: string[]; + }>[] +) { + const contactPages = ( + await queryAllDbResults( + notionClient, + { + database_id: contactsDbId, + }, + { cache: cacheConfig } + ) + ).filter(isFullPage); + + const contacts: Contact[] = contactPages.map((page) => ({ + notionId: page.id, + notionIdFamille: relationPropertyToPageId(page.properties, "Famille")!, + Nom: titlePropertyToText(page.properties, "Nom"), + AExercéUneMission: checkboxPropertyToBoolean( + page.properties, + "A exercé une mission" + ), + Missions: missions.filter((m) => m.ContactsNotionIds.includes(page.id)), + })); + return contacts; +} diff --git a/src/notion/fetch/fetchDepartements.ts b/src/notion/fetch/fetchDepartements.ts new file mode 100644 index 0000000..2e3123f --- /dev/null +++ b/src/notion/fetch/fetchDepartements.ts @@ -0,0 +1,26 @@ +import { Client, isFullPage } from "@notionhq/client"; +import { Departement } from "../../data/Departement"; +import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; +import { queryAllDbResults } from "../utils/queryAllDbResults"; +import { departementsDbId } from "./dbIds"; + +export async function fetchDepartements( + notionClient: Client, + cacheConfig: boolean | { ttl: number } +) { + const departementPages = ( + await queryAllDbResults( + notionClient, + { + database_id: departementsDbId, + }, + { cache: cacheConfig } + ) + ).filter(isFullPage); + + const departements: Departement[] = departementPages.map((page) => ({ + notionId: page.id, + name: titlePropertyToText(page.properties, "Nom"), + })); + return departements; +} diff --git a/src/notion/fetch/fetchEvenements.ts b/src/notion/fetch/fetchEvenements.ts new file mode 100644 index 0000000..ff6d291 --- /dev/null +++ b/src/notion/fetch/fetchEvenements.ts @@ -0,0 +1,53 @@ +import { Client, isFullPage } from "@notionhq/client"; +import { Amende } from "../../data/Amende"; +import { queryAllDbResults } from "../utils/queryAllDbResults"; +import { familEventsDbId } from "./dbIds"; +import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; +import { EvenementFamille } from "../../data/EvenementFamille"; +import { TypeEvenement } from "../../data/TypeEvenement"; +import { datePropertyToDate } from "../utils/properties/datePropertyToDate"; +import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId"; +import { selectPropertyToText } from "../utils/properties/selectPropertyToText"; +import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; +import { richTextPropertyToPlainText } from "../utils/text/richTextPropertyToPlainText"; + +export async function fetchEvenements( + notionClient: Client, + cacheConfig: boolean | { ttl: number }, + amendes: Amende[] +) { + const eventPages = ( + await queryAllDbResults( + notionClient, + { + database_id: familEventsDbId, + sorts: [{ property: "Date", direction: "ascending" }], + }, + { cache: cacheConfig } + ) + ).filter(isFullPage); + const familyEvents = eventPages.map((pageObjectResponse) => { + return buildFamilyEvent(pageObjectResponse, amendes); + }); + return familyEvents; +} +export function buildFamilyEvent( + page: PageObjectResponse, + amendes: Amende[] +): EvenementFamille { + const pageProperties = page.properties; + + const familyEvent: EvenementFamille = { + notionId: page.id, + Évènement: titlePropertyToText(pageProperties, "Évènement"), + Type: selectPropertyToText(pageProperties, "Type")! as TypeEvenement, + "Enfants concernés": richTextPropertyToPlainText( + pageProperties, + "Enfants concernés" + ), + Date: datePropertyToDate(pageProperties, "Date"), + notionIdFamille: relationPropertyToPageId(pageProperties, "Famille")!, + Amendes: amendes.filter((a) => a.notionIdEvenement === page.id), + }; + return familyEvent; +} diff --git a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts index 0afe0a6..89f8b17 100644 --- a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts +++ b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts @@ -1,47 +1,26 @@ -import { Client, isFullPage } from "@notionhq/client"; -import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; -import { ContexteEntreeDC } from "../../data/ContexteEntreeDC"; -import { EvenementFamille } from "../../data/EvenementFamille"; +import { Client } from "@notionhq/client"; 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"; -import { statusPropertyToText } from "../utils/properties/statusPropertyToText"; -import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; -import { CacheConfig, queryAllDbResults } from "../utils/queryAllDbResults"; -import { richTextPropertyToPlainText } from "../utils/text/richTextPropertyToPlainText"; -import { - contactsDbId, - departementsDbId, - familEventsDbId, - missionsDbId, -} from "./dbIds"; -import { - propContexteEntree, - familiesDbId, - propDerniereModification, - propPenal, - propSocial, -} from "./dbfamilleDesc"; -import { StatutPenal } from "../../data/StatutPenal"; -import { StatutSocial } from "../../data/StatutSocial"; +import { CacheConfig } from "../utils/queryAllDbResults"; import { Contact } from "../../data/Contact"; import { Mission } from "../../data/Mission"; -import { relationPropertyToPageIds } from "../utils/properties/relationPropertyToPageIds"; -import { checkboxPropertyToBoolean } from "../utils/properties/checkboxPropertyToBoolean"; import { Amende } from "../../data/Amende"; import { fetchAmendes } from "./fetchAmendes"; +import { Departement } from "../../data/Departement"; +import { fetchEvenements } from "./fetchEvenements"; +import { fetchContacts } from "./fetchContacts"; +import { fetchMissions } from "./fetchMissions"; +import { fetchDepartements } from "./fetchDepartements"; +import { fetchFamilles } from "./fetchFamilles"; -type Departement = { - notionId: string; - name: string; +export type FetchedData = { + familles: Famille[]; + departements: Departement[]; }; + export async function fetchFamiliesWithEventsFromNotion( notionClient: Client, cacheConfig: CacheConfig = { ttl: 3600 * 1000 } -): Promise { +): Promise { const departements: Departement[] = await fetchDepartements( notionClient, cacheConfig @@ -61,213 +40,5 @@ export async function fetchFamiliesWithEventsFromNotion( departements, contacts ); - return familles; -} - -async function fetchFamilles( - notionClient: Client, - cacheConfig: boolean | { ttl: number }, - familyEvents: Readonly<{ - notionId: string; - notionIdFamille: string; - Évènement: string; - Date: Date | null; - Type: TypeEvenement; - Amendes: Amende[]; - "Enfants concernés": string; - }>[], - departements: Departement[], - contacts: Readonly<{ - notionId: string; - notionIdFamille: string; - Nom: string; - Missions: Mission[]; - AExercéUneMission: boolean; - }>[] -) { - const familyPages = ( - await queryAllDbResults( - notionClient, - { - database_id: familiesDbId, - }, - { cache: cacheConfig } - ) - ).filter(isFullPage); - const familles: Famille[] = await Promise.all( - familyPages.map((pageObjectResponse) => { - return buildFamily( - pageObjectResponse, - familyEvents, - departements, - contacts - ); - }) - ); - return familles; -} - -async function fetchEvenements( - notionClient: Client, - cacheConfig: boolean | { ttl: number }, - amendes: Amende[] -) { - const eventPages = ( - await queryAllDbResults( - notionClient, - { - database_id: familEventsDbId, - sorts: [{ property: "Date", direction: "ascending" }], - }, - { cache: cacheConfig } - ) - ).filter(isFullPage); - const familyEvents = eventPages.map((pageObjectResponse) => { - return buildFamilyEvent(pageObjectResponse, amendes); - }); - return familyEvents; -} - -async function fetchContacts( - notionClient: Client, - cacheConfig: boolean | { ttl: number }, - missions: Readonly<{ - notionId: string; - Nom: string; - Equipe: string; - ContactsNotionIds: string[]; - }>[] -) { - const contactPages = ( - await queryAllDbResults( - notionClient, - { - database_id: contactsDbId, - }, - { cache: cacheConfig } - ) - ).filter(isFullPage); - - const contacts: Contact[] = contactPages.map((page) => ({ - notionId: page.id, - notionIdFamille: relationPropertyToPageId(page.properties, "Famille")!, - Nom: titlePropertyToText(page.properties, "Nom"), - AExercéUneMission: checkboxPropertyToBoolean( - page.properties, - "A exercé une mission" - ), - Missions: missions.filter((m) => m.ContactsNotionIds.includes(page.id)), - })); - return contacts; -} - -async function fetchMissions( - notionClient: Client, - cacheConfig: boolean | { ttl: number } -) { - const missionsPages = ( - await queryAllDbResults( - notionClient, - { - database_id: missionsDbId, - }, - { cache: cacheConfig } - ) - ).filter(isFullPage); - const missions: Mission[] = missionsPages.map((page) => ({ - notionId: page.id, - Nom: titlePropertyToText(page.properties, "Nom"), - Equipe: selectPropertyToText(page.properties, "Équipe")!, - ContactsNotionIds: relationPropertyToPageIds( - page.properties, - "📔 Contacts" - ), - })); - return missions; -} - -async function fetchDepartements( - notionClient: Client, - cacheConfig: boolean | { ttl: number } -) { - const departementPages = ( - await queryAllDbResults( - notionClient, - { - database_id: departementsDbId, - }, - { cache: cacheConfig } - ) - ).filter(isFullPage); - - const departements: Departement[] = departementPages.map((page) => ({ - notionId: page.id, - name: titlePropertyToText(page.properties, "Nom"), - })); - return departements; -} - -function buildFamilyEvent( - page: PageObjectResponse, - amendes: Amende[] -): EvenementFamille { - const pageProperties = page.properties; - - const familyEvent: EvenementFamille = { - notionId: page.id, - Évènement: titlePropertyToText(pageProperties, "Évènement"), - Type: selectPropertyToText(pageProperties, "Type")! as TypeEvenement, - "Enfants concernés": richTextPropertyToPlainText( - pageProperties, - "Enfants concernés" - ), - Date: datePropertyToDate(pageProperties, "Date"), - notionIdFamille: relationPropertyToPageId(pageProperties, "Famille")!, - Amendes: amendes.filter((a) => a.notionIdEvenement === page.id), - }; - return familyEvent; -} - -function buildFamily( - page: PageObjectResponse, - familyEvents: EvenementFamille[], - departements: Departement[], - contacts: Contact[] -): Famille { - const pageProperties = page.properties; - - const departementId = relationPropertyToPageId(pageProperties, "Département"); - const departement = departementId - ? departements.find((d) => d.notionId === departementId) - : null; - - const family: Famille = { - notionId: page.id, - Titre: titlePropertyToText(pageProperties, ""), - Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille, - ContexteEntree: selectPropertyToText( - pageProperties, - propContexteEntree - ) as ContexteEntreeDC, - Integration: datePropertyToDate(pageProperties, "Intégration"), - Sortie: datePropertyToDate(pageProperties, "Sortie"), - Evenements: familyEvents.filter((fe) => fe.notionIdFamille === page.id), - // Ces 4 propriétés seront peuplés après le data consistency check - EvenementsDates: [], - EvenementsEL: [], - EvenementsAvantEL: [], - EvenementsApresEL: [], - Departement: departement?.name || null, - DerniereModification: datePropertyToDate( - pageProperties, - propDerniereModification - )!, - Penal: statusPropertyToText(pageProperties, propPenal) as StatutPenal, - Social: statusPropertyToText(pageProperties, propSocial) as StatutSocial, - Missions: contacts - .filter((c) => c.notionIdFamille === page.id) - .flatMap((c) => c.Missions), - Contacts: contacts.filter((c) => c.notionIdFamille === page.id), - }; - return family; + return { familles, departements }; } diff --git a/src/notion/fetch/fetchFamilles.ts b/src/notion/fetch/fetchFamilles.ts new file mode 100644 index 0000000..a25ec50 --- /dev/null +++ b/src/notion/fetch/fetchFamilles.ts @@ -0,0 +1,112 @@ +import { Client, isFullPage } from "@notionhq/client"; +import { Amende } from "../../data/Amende"; +import { Departement } from "../../data/Departement"; +import { Famille } from "../../data/Famille"; +import { Mission } from "../../data/Mission"; +import { TypeEvenement } from "../../data/TypeEvenement"; +import { queryAllDbResults } from "../utils/queryAllDbResults"; +import { + familiesDbId, + propContexteEntree, + propDerniereModification, + propPenal, + propSocial, +} from "./dbfamilleDesc"; +import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; +import { EvenementFamille } from "../../data/EvenementFamille"; +import { Contact } from "../../data/Contact"; +import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId"; +import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; +import { statusPropertyToText } from "../utils/properties/statusPropertyToText"; +import { selectPropertyToText } from "../utils/properties/selectPropertyToText"; +import { StatutFamille } from "../../data/StatutFamille"; +import { ContexteEntreeDC } from "../../data/ContexteEntreeDC"; +import { datePropertyToDate } from "../utils/properties/datePropertyToDate"; +import { StatutPenal } from "../../data/StatutPenal"; +import { StatutSocial } from "../../data/StatutSocial"; + +export async function fetchFamilles( + notionClient: Client, + cacheConfig: boolean | { ttl: number }, + familyEvents: Readonly<{ + notionId: string; + notionIdFamille: string; + Évènement: string; + Date: Date | null; + Type: TypeEvenement; + Amendes: Amende[]; + "Enfants concernés": string; + }>[], + departements: Departement[], + contacts: Readonly<{ + notionId: string; + notionIdFamille: string; + Nom: string; + Missions: Mission[]; + AExercéUneMission: boolean; + }>[] +) { + const familyPages = ( + await queryAllDbResults( + notionClient, + { + database_id: familiesDbId, + }, + { cache: cacheConfig } + ) + ).filter(isFullPage); + const familles: Famille[] = await Promise.all( + familyPages.map((pageObjectResponse) => { + return buildFamily( + pageObjectResponse, + familyEvents, + departements, + contacts + ); + }) + ); + return familles; +} +function buildFamily( + page: PageObjectResponse, + familyEvents: EvenementFamille[], + departements: Departement[], + contacts: Contact[] +): Famille { + const pageProperties = page.properties; + + const departementId = relationPropertyToPageId(pageProperties, "Département"); + const departement = departementId + ? departements.find((d) => d.notionId === departementId) + : null; + + const family: Famille = { + notionId: page.id, + Titre: titlePropertyToText(pageProperties, ""), + Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille, + ContexteEntree: selectPropertyToText( + pageProperties, + propContexteEntree + ) as ContexteEntreeDC, + Integration: datePropertyToDate(pageProperties, "Intégration"), + Sortie: datePropertyToDate(pageProperties, "Sortie"), + Evenements: familyEvents.filter((fe) => fe.notionIdFamille === page.id), + // Ces 4 propriétés seront peuplés après le data consistency check + EvenementsDates: [], + EvenementsEL: [], + EvenementsAvantEL: [], + EvenementsApresEL: [], + Departement: departement?.name || null, + DerniereModification: datePropertyToDate( + pageProperties, + propDerniereModification + )!, + Penal: statusPropertyToText(pageProperties, propPenal) as StatutPenal, + Social: statusPropertyToText(pageProperties, propSocial) as StatutSocial, + Missions: contacts + .filter((c) => c.notionIdFamille === page.id) + .flatMap((c) => c.Missions), + Contacts: contacts.filter((c) => c.notionIdFamille === page.id), + }; + return family; +} diff --git a/src/notion/fetch/fetchMissions.ts b/src/notion/fetch/fetchMissions.ts new file mode 100644 index 0000000..bf1f474 --- /dev/null +++ b/src/notion/fetch/fetchMissions.ts @@ -0,0 +1,32 @@ +import { Client, isFullPage } from "@notionhq/client"; +import { Mission } from "../../data/Mission"; +import { relationPropertyToPageIds } from "../utils/properties/relationPropertyToPageIds"; +import { selectPropertyToText } from "../utils/properties/selectPropertyToText"; +import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; +import { queryAllDbResults } from "../utils/queryAllDbResults"; +import { missionsDbId } from "./dbIds"; + +export async function fetchMissions( + notionClient: Client, + cacheConfig: boolean | { ttl: number } +) { + const missionsPages = ( + await queryAllDbResults( + notionClient, + { + database_id: missionsDbId, + }, + { cache: cacheConfig } + ) + ).filter(isFullPage); + const missions: Mission[] = missionsPages.map((page) => ({ + notionId: page.id, + Nom: titlePropertyToText(page.properties, "Nom"), + Equipe: selectPropertyToText(page.properties, "Équipe")!, + ContactsNotionIds: relationPropertyToPageIds( + page.properties, + "📔 Contacts" + ), + })); + return missions; +} diff --git a/src/notion/publish/publishStatsDepartementales.ts b/src/notion/publish/publishStatsDepartementales.ts new file mode 100644 index 0000000..c2fd67d --- /dev/null +++ b/src/notion/publish/publishStatsDepartementales.ts @@ -0,0 +1,119 @@ +import { Client } from "@notionhq/client"; +import { + StatDepartement, + StatDepartementales, +} from "../../statistiques/v2/departemental/computeStatDepartement"; +import { + CreatePageResponse, + QueryDatabaseResponse, + UpdatePageParameters, +} from "@notionhq/client/build/src/api-endpoints"; + +export async function publishStatsDepartementales( + notionClient: Client, + deptStatsDbId: string, + statsDepartementales: StatDepartementales +): Promise { + console.log("Publication des statistiques départementales..."); + Object.entries(statsDepartementales).forEach( + async ([deptName, stat]: [string, StatDepartement]) => { + const deptStatPageId: string = + (await findDeptStatPageId(notionClient, deptStatsDbId, deptName)) || + (await createEmptyDeptStatPageId( + notionClient, + deptStatsDbId, + deptName + )); + await updateDeptStatPage(notionClient, deptStatPageId, stat); + } + ); +} +async function updateDeptStatPage( + notionClient: Client, + + deptStatPageId: string, + stat: StatDepartement +): Promise { + const props = { + "Nb Familles": { + number: stat.nbFamilles, + }, + "Pénale ?": { + checkbox: stat.hasProcedurePenale, + }, + "% MED": { + number: stat.pourcentageMED, + }, + "% Pénale hors Gendarmerie": { + number: stat.pourcentageProcedurePenaleHorsGendarmerie, + }, + "% TC": { + number: stat.pourcentageTC, + }, + "% TP": { + number: stat.pourcentageTP, + }, + "Civile ?": { + checkbox: stat.hasProcedureCivile, + }, + "% IP": { + number: stat.pourcentageIP, + }, + "% MJIE ou AEMO": { + number: stat.pourcentageMJIEouAEMO, + }, + } as UpdatePageParameters["properties"]; + await notionClient.pages.update({ + page_id: deptStatPageId, + properties: props, + }); +} + +async function findDeptStatPageId( + notionClient: Client, + deptStatsDbId: string, + deptName: string +): Promise { + const res: QueryDatabaseResponse = await notionClient.databases.query({ + database_id: deptStatsDbId, + filter: { + property: "Département", + rich_text: { + equals: deptName, + }, + }, + }); + if (res.results.length === 0) { + return undefined; + } else if (res.results.length === 1) { + return res.results[0].id; + } else { + throw new Error( + `Plusieurs pages trouvées pour le département ${deptName} dans la base de données des statistiques départementales.` + ); + } +} + +async function createEmptyDeptStatPageId( + notionClient: Client, + deptStatsDbId: string, + deptName: string +): Promise { + const res: CreatePageResponse = await notionClient.pages.create({ + parent: { + database_id: deptStatsDbId, + }, + properties: { + Département: { + title: [ + { + text: { + content: deptName, + }, + }, + ], + }, + }, + }); + return res.id; +} diff --git a/src/statistiques/v2/departemental/computeStatDepartement.ts b/src/statistiques/v2/departemental/computeStatDepartement.ts new file mode 100644 index 0000000..a19effa --- /dev/null +++ b/src/statistiques/v2/departemental/computeStatDepartement.ts @@ -0,0 +1,106 @@ +import { groupBy } from "lodash"; +import { Departement } from "../../../data/Departement"; +import { + isInformationPreoccupante, + isProcedureCivile, +} from "../../../data/EvenementFamille"; +import { Famille } from "../../../data/Famille"; +import { + isEvtTypeProcedurePenale, + isEvtTypeProcedurePenaleHorsGendarmerie, +} from "../../../data/TypeEvenementsPenal"; +import { filterFamillesWithOneOfEvenements } from "../filterFamillesWithOneOfEvenements"; +import { filterFamillesWithOneOfEvenementsOfType } from "../filterFamillesWithOneOfEvenementsOfType"; +import { percent } from "../math/percent"; + +export type StatDepartementales = { + [departement: string]: StatDepartement; +}; + +export function computeStatsDepartementales( + familles: Famille[], + departements: Departement[] +): StatDepartementales { + const famillesByDepartement: Record = groupBy( + familles, + (f) => f.Departement + ); + return Object.fromEntries( + departements.map((d) => [ + d.name, + computeStatsDepartement(famillesByDepartement[d.name] || []), + ]) + ); +} + +export type StatDepartement = { + nbFamilles: number; + hasProcedurePenale: boolean; + pourcentageMED: number | undefined; + pourcentageProcedurePenaleHorsGendarmerie: number | undefined; + pourcentageTC: number | undefined; + pourcentageTP: number | undefined; + hasProcedureCivile: boolean; + pourcentageIP: number | undefined; + pourcentageMJIEouAEMO: number | undefined; +}; + +function computeStatsDepartement( + famillesDepartements: Famille[] +): StatDepartement { + return { + nbFamilles: famillesDepartements.length, + hasProcedurePenale: + filterFamillesWithOneOfEvenements(famillesDepartements, (e) => + isEvtTypeProcedurePenale(e.Type) + ).length > 0, + pourcentageMED: percent( + filterFamillesWithOneOfEvenementsOfType( + famillesDepartements, + "Mise en demeure de scolarisation" + ).length, + famillesDepartements.length + ), + pourcentageProcedurePenaleHorsGendarmerie: percent( + filterFamillesWithOneOfEvenements(famillesDepartements, (e) => + isEvtTypeProcedurePenaleHorsGendarmerie(e.Type) + ).length, + famillesDepartements.length + ), + pourcentageTC: percent( + filterFamillesWithOneOfEvenements( + famillesDepartements, + (e) => e.Type === "Tribunal de police judiciaire" + ).length, + famillesDepartements.length + ), + pourcentageTP: percent( + filterFamillesWithOneOfEvenements( + famillesDepartements, + (e) => e.Type === "Tribunal de police judiciaire" + ).length, + famillesDepartements.length + ), + hasProcedureCivile: + filterFamillesWithOneOfEvenements(famillesDepartements, (e) => + isProcedureCivile(e) + ).length > 0, + pourcentageIP: percent( + filterFamillesWithOneOfEvenements(famillesDepartements, (e) => + isInformationPreoccupante(e) + ).length, + famillesDepartements.length + ), + pourcentageMJIEouAEMO: percent( + famillesDepartements.filter( + (f) => + f.Social === "MJIE" || + f.Social === "AEMO" || + f.EvenementsEL.some( + (e) => e.Type === "Classement suite AEMO MIJE ou..." + ) + ).length, + famillesDepartements.length + ), + }; +} diff --git a/src/statistiques/v2/penales/computeStatDepartement.ts b/src/statistiques/v2/penales/computeStatDepartement.ts new file mode 100644 index 0000000..9eaa7c1 --- /dev/null +++ b/src/statistiques/v2/penales/computeStatDepartement.ts @@ -0,0 +1,42 @@ +import { Famille } from "../../../data/Famille"; +import { isEvtTypeProcedurePenaleHorsGendarmerie } from "../../../data/TypeEvenementsPenal"; +import { filterFamillesWithOneOfEvenements } from "../filterFamillesWithOneOfEvenements"; +import { filterFamillesWithOneOfEvenementsOfType } from "../filterFamillesWithOneOfEvenementsOfType"; +import { percent } from "../math/percent"; +import { nbFamillesAvecPagesLiees } from "../nbFamillesAvecPagesLiees"; +import { StatsPenales } from "./StatsPenales"; + +export 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 + ), + }; +} diff --git a/src/statistiques/v2/penales/computeStatsPenales.ts b/src/statistiques/v2/penales/computeStatsPenales.ts index ea12498..563df44 100644 --- a/src/statistiques/v2/penales/computeStatsPenales.ts +++ b/src/statistiques/v2/penales/computeStatsPenales.ts @@ -23,9 +23,8 @@ import { } from "./computeFamilleAvecInfosProceduresPenales"; import { computeIntervalMedGendarmerieOuProcureur } from "./intervals/computeIntervalMedGendarmerieOuProcureur"; import { groupBy } from "lodash"; -import { percent } from "../math/percent"; -import { isEvtTypeProcedurePenaleHorsGendarmerie } from "../../../data/TypeEvenementsPenal"; import { computeStatsAmendes } from "./computeStatsAmendes"; +import { computeStatDepartement } from "./computeStatDepartement"; export type FamilleAvecInfoTribunaux = Famille & { infoTribunaux: InfoTribunalCorrectionnel[]; @@ -180,41 +179,6 @@ export function computeStatsPenales(familles: Famille[]): 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( famillesResistantesOuEx: Famille[] ): StatsPenales["procureur"]["crpc"] {