diff --git a/src/data/Family.ts b/src/data/Family.ts index 59a0e32..aa5e43b 100644 --- a/src/data/Family.ts +++ b/src/data/Family.ts @@ -1,7 +1,25 @@ import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping"; import { Period } from "../period/Period"; -export type FamilyStatus = +export type Family = { + notionId: string; + Titre: string; + Statut: StatutFamille; + Integration: Date | null; + Sortie: Date | null; + Evenements: FamilyEvent[]; +}; + +export type FamilyEvent = { + notionId: string; + notionIdFamille: string; + Évènement: string; + Date: Date | null; + Type: string; + "Enfants concernés": string; +}; + +export type StatutFamille = | "Résistant.e" | "Ex résistant·e·s" | "À préciser" @@ -11,43 +29,31 @@ export type FamilyStatus = | "Déclaration Validée - Attente éléments" | "Abdandon" | "Incompatible"; -export type Family = { - notionId: string; - title: string; - status: FamilyStatus; - - startResistsant: Date | null; - endResistant: Date | null; - evenements: FamilyEvent[]; -}; export function isResistant(family: Family): boolean { return ( - family.status === "Résistant.e" && - family.startResistsant !== null && - family.endResistant === null + family.Statut === "Résistant.e" && + family.Integration !== null && + family.Sortie === null ); } export function isExResistant(family: Family): boolean { return ( - family.status === "Ex résistant·e·s" && - family.startResistsant !== null && - family.endResistant !== null + family.Statut === "Ex résistant·e·s" && + family.Integration !== null && + family.Sortie !== null ); } export function isResistantAtDate(family: Family, date: Date): boolean { - if ( - isResistant(family) && - family.startResistsant!.getTime() <= date.getTime() - ) { + if (isResistant(family) && family.Integration!.getTime() <= date.getTime()) { return true; } if ( isExResistant(family) && - family.startResistsant!.getTime() <= date.getTime() && - family.endResistant!.getTime() > date.getTime() + family.Integration!.getTime() <= date.getTime() && + family.Sortie!.getTime() > date.getTime() ) { return true; } @@ -62,25 +68,20 @@ export function isResistantOverPeriod(family: Family, period: Period): boolean { ); } -function periodOfResistance(family: Family): Period | null { +export function periodOfResistance(family: Family): Period | null { if (isResistant(family)) { const periodResistant: Period = { - start: family.startResistsant!, + start: family.Integration!, end: new Date(Date.now()), }; return periodResistant; } if (isExResistant(family)) { const periodResistant: Period = { - start: family.startResistsant!, - end: family.endResistant!, + start: family.Integration!, + end: family.Sortie!, }; return periodResistant; } return null; } - -export type FamilyEvent = { - date: Date; - type: string; -}; diff --git a/src/data/checkDataConsistency.ts b/src/data/checkDataConsistency.ts index 65d7378..90ead30 100644 --- a/src/data/checkDataConsistency.ts +++ b/src/data/checkDataConsistency.ts @@ -13,49 +13,49 @@ export type ConsistencyIssue = { function checkFamilyDataConsistency(family: Family) { const consistencyIssues: ConsistencyIssue[] = []; - if (family.status === "Résistant.e") { - if (family.startResistsant === null) { + if (family.Statut === "Résistant.e") { + if (family.Integration === null) { consistencyIssues.push({ - familyId: family.title, + familyId: family.Titre, issueType: "Résistant.e without startResistant", }); } - if (family.endResistant !== null) { + if (family.Sortie !== null) { consistencyIssues.push({ - familyId: family.title, + familyId: family.Titre, issueType: "Résistant.e with endResistant!!", }); } - } else if (family.status === "Ex résistant·e·s") { - if (family.startResistsant === null) { + } else if (family.Statut === "Ex résistant·e·s") { + if (family.Integration === null) { consistencyIssues.push({ - familyId: family.title, + familyId: family.Titre, issueType: "Ex résistant.e.s without startResistant", }); } - if (family.endResistant === null) { + if (family.Sortie === null) { consistencyIssues.push({ - familyId: family.title, + familyId: family.Titre, issueType: "Ex résistant.e.s without endResistant", }); } - if (family.startResistsant!.getTime() > family.endResistant!.getTime()) { + if (family.Integration!.getTime() > family.Sortie!.getTime()) { consistencyIssues.push({ - familyId: family.title, + familyId: family.Titre, issueType: "startResistsant > endResistant ", }); } } else { - if (family.startResistsant !== null) { + if (family.Integration !== null) { consistencyIssues.push({ - familyId: family.title, - issueType: family.status + " with startResistant", + familyId: family.Titre, + issueType: family.Statut + " with startResistant", }); } - if (family.endResistant !== null) { + if (family.Sortie !== null) { consistencyIssues.push({ - familyId: family.title, - issueType: family.status + " with endResistant", + familyId: family.Titre, + issueType: family.Statut + " with endResistant", }); } } diff --git a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts index e29df69..8879196 100644 --- a/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts +++ b/src/notion/fetch/fetchFamiliesWithEventsFromNotion.ts @@ -1,50 +1,73 @@ -import { Client } from "@notionhq/client"; -import { - PageObjectResponse, - QueryDatabaseParameters, -} from "@notionhq/client/build/src/api-endpoints"; -import { Family, FamilyStatus } from "../../data/Family"; +import { Client, isFullPage } from "@notionhq/client"; +import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; +import { Family, FamilyEvent, StatutFamille } from "../../data/Family"; 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 copy"; +import { titlePropertyToText } from "../utils/properties/titlePropertyToText"; import { queryAllDbResults } from "../utils/queryAllDbResults"; -import { assertFullPage } from "../utils/types/assertFullPage"; +import { richTextPropertyToPlainText } from "../utils/text/richTextPropertyToPlainText"; export async function fetchFamiliesWithEventsFromNotion( notionClient: Client ): Promise { const familiesDbId: string = "5b69e02b296d4a578f8c8ab7fe8b05da"; - const dbQuery: QueryDatabaseParameters = { - database_id: familiesDbId, - /*filter: { - property: "Statut", - status: { - equals: "Résistant.e", - }, - },*/ - }; + const familEventsDbId: string = "c4d434b4603c4481a4d445618ecdf999"; + + const eventPages = ( + await queryAllDbResults(notionClient, { + database_id: familEventsDbId, + }) + ).filter(isFullPage); + const familyPages = ( + await queryAllDbResults(notionClient, { + database_id: familiesDbId, + }) + ).filter(isFullPage); + + const familyEvents = eventPages.map((pageObjectResponse) => { + return buildFamilyEvent(pageObjectResponse); + }); - const results = await queryAllDbResults(notionClient, dbQuery); const families: Family[] = await Promise.all( - results.map((pageObjectResponse) => { - assertFullPage(pageObjectResponse); - return buildFamily(pageObjectResponse); + familyPages.map((pageObjectResponse) => { + return buildFamily(pageObjectResponse, familyEvents); }) ); return families; } -function buildFamily(page: PageObjectResponse): Family { +function buildFamilyEvent(page: PageObjectResponse): FamilyEvent { + const pageProperties = page.properties; + + const familyEvent: FamilyEvent = { + notionId: page.id, + Évènement: titlePropertyToText(pageProperties, "Évènement"), + Type: selectPropertyToText(pageProperties, "Type")!, + "Enfants concernés": richTextPropertyToPlainText( + pageProperties, + "Enfants concernés" + ), + Date: datePropertyToDate(pageProperties, "Date"), + notionIdFamille: relationPropertyToPageId(pageProperties, "Famille")!, + }; + return familyEvent; +} + +function buildFamily( + page: PageObjectResponse, + familyEvents: FamilyEvent[] +): Family { const pageProperties = page.properties; - // TODO Fetch Family Events const family: Family = { notionId: page.id, - title: titlePropertyToText(pageProperties, ""), - status: statusPropertyToText(pageProperties, "Statut") as FamilyStatus, - startResistsant: datePropertyToDate(pageProperties, "Intégration"), - endResistant: datePropertyToDate(pageProperties, "Sortie"), - evenements: [], + Titre: titlePropertyToText(pageProperties, ""), + Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille, + Integration: datePropertyToDate(pageProperties, "Intégration"), + Sortie: datePropertyToDate(pageProperties, "Sortie"), + Evenements: familyEvents.filter((fe) => fe.notionIdFamille === page.id), }; return family; } diff --git a/src/notion/publish/publishCurrentStats.ts b/src/notion/publish/publishCurrentStats.ts index 8470c48..ff610ed 100644 --- a/src/notion/publish/publishCurrentStats.ts +++ b/src/notion/publish/publishCurrentStats.ts @@ -41,10 +41,18 @@ export async function publishCurrentStats( block_id: statsPageId, after: currentStatsHeadingBlock.id, children: [ - currentStatBlock("Nb Famille Résistante", stats.resistantsCount), + currentStatBlock("Nb Famille Résistante", stats.nbFamilleResistantes), currentStatBlock( "Nb Famille Résistante ou Ex-Résistante", - stats.resistantsOrExCount + stats.nbFamilleResistantesOrEx + ), + currentStatBlock( + "Durée Moyenne Résistance (jours)", + stats.dureeMoyenneResistance + ), + currentStatBlock( + "Durée Médiane Résistance (jours)", + stats.dureeMedianeResistance ), ], }); diff --git a/src/notion/publish/publishStatisticsToNotion.ts b/src/notion/publish/publishStatisticsToNotion.ts index b4abacf..cc1083b 100644 --- a/src/notion/publish/publishStatisticsToNotion.ts +++ b/src/notion/publish/publishStatisticsToNotion.ts @@ -15,9 +15,9 @@ export async function publishStatisticsToNotion( ) { await publishCurrentStats(notionClient, stats); - await publishPeriodStats(notionClient, yearStatsDb, stats.years); + await publishPeriodStats(notionClient, yearStatsDb, stats.annees); - await publishPeriodStats(notionClient, monthStatsDb, stats.months); + await publishPeriodStats(notionClient, monthStatsDb, stats.mois); } async function publishPeriodStats( @@ -50,10 +50,12 @@ async function publishPeriodStats( }, ], }, - "Nb Famille Résistante": numberProp(stat.resistantsCount), - "Nb Famille Résistante - Evol": numberProp(stat.resistantsCountEvol), + "Nb Famille Résistante": numberProp(stat.nbFamilleResistantes), + "Nb Famille Résistante - Evol": numberProp( + stat.nbFamilleResistantesEvol + ), "Nb Famille Résistante - Evol %": numberProp( - stat.resistantsCountEvolPercent + stat.nbFamilleResistantesEvolPercent ), }, }); diff --git a/src/notion/utils/properties/relationPropertyToPageId.ts b/src/notion/utils/properties/relationPropertyToPageId.ts new file mode 100644 index 0000000..bcb33ee --- /dev/null +++ b/src/notion/utils/properties/relationPropertyToPageId.ts @@ -0,0 +1,18 @@ +import { PageProperties } from "../types/PageProperties"; +import { extractPagePropertyValue } from "./extractPagePropertyValue"; + +export function relationPropertyToPageId( + pageProperties: PageProperties, + propName: string +): string | null { + const propValue = extractPagePropertyValue(pageProperties, propName); + if (propValue.type !== "relation") { + throw new Error( + `Property ${propName} was expected to have type "relation" but got "${propValue.type}".` + ); + } + if (propValue.relation.length === 0) { + return null; + } + return propValue.relation[0].id; +} diff --git a/src/notion/utils/properties/selectPropertyToText.ts b/src/notion/utils/properties/selectPropertyToText.ts new file mode 100644 index 0000000..792d0a8 --- /dev/null +++ b/src/notion/utils/properties/selectPropertyToText.ts @@ -0,0 +1,18 @@ +import { PageProperties } from "../types/PageProperties"; +import { extractPagePropertyValue } from "./extractPagePropertyValue"; + +export function selectPropertyToText( + pageProperties: PageProperties, + propName: string +): string | null { + const propValue = extractPagePropertyValue(pageProperties, propName); + if (propValue.type !== "select") { + throw new Error( + `Property ${propName} was expected to have type "select" but got "${propValue.type}".` + ); + } + if (propValue.select === null) { + return null; + } + return propValue.select!.name; +} diff --git a/src/notion/utils/properties/statusPropertyToText.ts b/src/notion/utils/properties/statusPropertyToText.ts index 034f95e..dad9519 100644 --- a/src/notion/utils/properties/statusPropertyToText.ts +++ b/src/notion/utils/properties/statusPropertyToText.ts @@ -4,12 +4,15 @@ import { extractPagePropertyValue } from "./extractPagePropertyValue"; export function statusPropertyToText( pageProperties: PageProperties, propName: string -): string { +): string | null { const propValue = extractPagePropertyValue(pageProperties, propName); if (propValue.type !== "status") { throw new Error( - `Property ${propName} was expected to have type "title" but got "${propValue.type}".` + `Property ${propName} was expected to have type "status" but got "${propValue.type}".` ); } + if (propValue.status === null) { + return null; + } return propValue.status!.name; } diff --git a/src/notion/utils/properties/titlePropertyToText copy.ts b/src/notion/utils/properties/titlePropertyToText.ts similarity index 100% rename from src/notion/utils/properties/titlePropertyToText copy.ts rename to src/notion/utils/properties/titlePropertyToText.ts diff --git a/src/period/daysInPeriod.ts b/src/period/daysInPeriod.ts new file mode 100644 index 0000000..cf98483 --- /dev/null +++ b/src/period/daysInPeriod.ts @@ -0,0 +1,6 @@ +import { differenceInDays } from "date-fns"; +import { Period } from "./Period"; + +export function daysInPeriod(period: Period): number { + return differenceInDays(period.end, period.start); +} diff --git a/src/statistiques/ELStats.ts b/src/statistiques/ELStats.ts index 593968b..15861b2 100644 --- a/src/statistiques/ELStats.ts +++ b/src/statistiques/ELStats.ts @@ -1,21 +1,19 @@ export type ELStats = { - /** Current Resistants Count */ - resistantsCount: number; + nbFamilleResistantes: number; /** Includes Ancient resistants */ - resistantsOrExCount: number; + nbFamilleResistantesOrEx: number; - /** - * Resistant count per Year - * Family Partially resistant over a year are counted in. - */ - years: ELPeriodStats[]; + dureeMoyenneResistance: number; + dureeMedianeResistance: number; - months: ELPeriodStats[]; + annees: ELPeriodStats[]; + + mois: ELPeriodStats[]; }; export type ELPeriodStats = { periodId: string; - resistantsCount: number; - resistantsCountEvol: number; - resistantsCountEvolPercent: number; + nbFamilleResistantes: number; + nbFamilleResistantesEvol: number; + nbFamilleResistantesEvolPercent: number; }; diff --git a/src/statistiques/computeELPeriodStats.ts b/src/statistiques/computeELPeriodStats.ts index 6037c03..ad2572d 100644 --- a/src/statistiques/computeELPeriodStats.ts +++ b/src/statistiques/computeELPeriodStats.ts @@ -15,14 +15,14 @@ export function computeELPeriodStats( const stats: ELPeriodStats = { periodId: period.id, - resistantsCount, - resistantsCountEvol: evol( + nbFamilleResistantes: resistantsCount, + nbFamilleResistantesEvol: evol( resistantsCount, - previousELPeriodStats?.resistantsCount + previousELPeriodStats?.nbFamilleResistantes ), - resistantsCountEvolPercent: evolPercent( + nbFamilleResistantesEvolPercent: evolPercent( resistantsCount, - previousELPeriodStats?.resistantsCount + previousELPeriodStats?.nbFamilleResistantes ), }; periodStats.push(stats); diff --git a/src/statistiques/computeELStats.ts b/src/statistiques/computeELStats.ts index 4dd542a..32e4437 100644 --- a/src/statistiques/computeELStats.ts +++ b/src/statistiques/computeELStats.ts @@ -1,14 +1,27 @@ -import { Family, isExResistant, isResistant } from "../data/Family"; +import { + Family, + isExResistant, + isResistant, + periodOfResistance, +} from "../data/Family"; +import { daysInPeriod } from "../period/daysInPeriod"; import { ELStats } from "./ELStats"; import { computeELPeriodStats } from "./computeELPeriodStats"; import { generateELMonths } from "./generateELMonths"; import { generateELYears } from "./generateELYears"; +import { average } from "./math/average"; +import { median } from "./math/median"; export function computeELStats(families: Family[]): ELStats { const resistantsCount = families.filter(isResistant).length; - const resistantsOrExCount = families.filter( + const resistantsOrEx = families.filter( (f) => isResistant(f) || isExResistant(f) - ).length; + ); + const durations = resistantsOrEx.map((f) => + daysInPeriod(periodOfResistance(f)!) + ); + const dureeMoyenne = average(durations); + const dureeMediane = median(durations); const elYears = generateELYears(); const yearsStats = computeELPeriodStats(families, elYears); @@ -17,9 +30,11 @@ export function computeELStats(families: Family[]): ELStats { const monthsStats = computeELPeriodStats(families, months); return { - resistantsCount, - resistantsOrExCount, - years: yearsStats, - months: monthsStats, + nbFamilleResistantes: resistantsCount, + nbFamilleResistantesOrEx: resistantsOrEx.length, + dureeMoyenneResistance: dureeMoyenne, + dureeMedianeResistance: dureeMediane, + annees: yearsStats, + mois: monthsStats, }; } diff --git a/src/statistiques/math/average.ts b/src/statistiques/math/average.ts new file mode 100644 index 0000000..9c8f781 --- /dev/null +++ b/src/statistiques/math/average.ts @@ -0,0 +1,5 @@ +export function average(values: number[]): number { + if (values.length === 0) return NaN; + + return values.reduce((a, b) => a + b) / values.length; +} diff --git a/src/statistiques/math/median.ts b/src/statistiques/math/median.ts new file mode 100644 index 0000000..dfbc6f9 --- /dev/null +++ b/src/statistiques/math/median.ts @@ -0,0 +1,11 @@ +export function median(values: number[]): number { + if (values.length === 0) return NaN; + + const sorted = [...values].sort((a, b) => a - b); + + const half = Math.floor(sorted.length / 2); + + return sorted.length % 2 + ? sorted[half] + : (sorted[half - 1] + sorted[half]) / 2; +}