feat: ajoute dureeMoyenneResistance dureeMedianeResistance

This commit is contained in:
sebastien.arod@gmail.com 2024-06-02 21:00:42 +02:00
parent 74b7fa8f0e
commit 962bdcde80
15 changed files with 218 additions and 110 deletions

View file

@ -1,7 +1,25 @@
import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping"; import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping";
import { Period } from "../period/Period"; 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" | "Résistant.e"
| "Ex résistant·e·s" | "Ex résistant·e·s"
| "À préciser" | "À préciser"
@ -11,43 +29,31 @@ export type FamilyStatus =
| "Déclaration Validée - Attente éléments" | "Déclaration Validée - Attente éléments"
| "Abdandon" | "Abdandon"
| "Incompatible"; | "Incompatible";
export type Family = {
notionId: string;
title: string;
status: FamilyStatus;
startResistsant: Date | null;
endResistant: Date | null;
evenements: FamilyEvent[];
};
export function isResistant(family: Family): boolean { export function isResistant(family: Family): boolean {
return ( return (
family.status === "Résistant.e" && family.Statut === "Résistant.e" &&
family.startResistsant !== null && family.Integration !== null &&
family.endResistant === null family.Sortie === null
); );
} }
export function isExResistant(family: Family): boolean { export function isExResistant(family: Family): boolean {
return ( return (
family.status === "Ex résistant·e·s" && family.Statut === "Ex résistant·e·s" &&
family.startResistsant !== null && family.Integration !== null &&
family.endResistant !== null family.Sortie !== null
); );
} }
export function isResistantAtDate(family: Family, date: Date): boolean { export function isResistantAtDate(family: Family, date: Date): boolean {
if ( if (isResistant(family) && family.Integration!.getTime() <= date.getTime()) {
isResistant(family) &&
family.startResistsant!.getTime() <= date.getTime()
) {
return true; return true;
} }
if ( if (
isExResistant(family) && isExResistant(family) &&
family.startResistsant!.getTime() <= date.getTime() && family.Integration!.getTime() <= date.getTime() &&
family.endResistant!.getTime() > date.getTime() family.Sortie!.getTime() > date.getTime()
) { ) {
return true; 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)) { if (isResistant(family)) {
const periodResistant: Period = { const periodResistant: Period = {
start: family.startResistsant!, start: family.Integration!,
end: new Date(Date.now()), end: new Date(Date.now()),
}; };
return periodResistant; return periodResistant;
} }
if (isExResistant(family)) { if (isExResistant(family)) {
const periodResistant: Period = { const periodResistant: Period = {
start: family.startResistsant!, start: family.Integration!,
end: family.endResistant!, end: family.Sortie!,
}; };
return periodResistant; return periodResistant;
} }
return null; return null;
} }
export type FamilyEvent = {
date: Date;
type: string;
};

View file

@ -13,49 +13,49 @@ export type ConsistencyIssue = {
function checkFamilyDataConsistency(family: Family) { function checkFamilyDataConsistency(family: Family) {
const consistencyIssues: ConsistencyIssue[] = []; const consistencyIssues: ConsistencyIssue[] = [];
if (family.status === "Résistant.e") { if (family.Statut === "Résistant.e") {
if (family.startResistsant === null) { if (family.Integration === null) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: "Résistant.e without startResistant", issueType: "Résistant.e without startResistant",
}); });
} }
if (family.endResistant !== null) { if (family.Sortie !== null) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: "Résistant.e with endResistant!!", issueType: "Résistant.e with endResistant!!",
}); });
} }
} else if (family.status === "Ex résistant·e·s") { } else if (family.Statut === "Ex résistant·e·s") {
if (family.startResistsant === null) { if (family.Integration === null) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: "Ex résistant.e.s without startResistant", issueType: "Ex résistant.e.s without startResistant",
}); });
} }
if (family.endResistant === null) { if (family.Sortie === null) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: "Ex résistant.e.s without endResistant", issueType: "Ex résistant.e.s without endResistant",
}); });
} }
if (family.startResistsant!.getTime() > family.endResistant!.getTime()) { if (family.Integration!.getTime() > family.Sortie!.getTime()) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: "startResistsant > endResistant ", issueType: "startResistsant > endResistant ",
}); });
} }
} else { } else {
if (family.startResistsant !== null) { if (family.Integration !== null) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: family.status + " with startResistant", issueType: family.Statut + " with startResistant",
}); });
} }
if (family.endResistant !== null) { if (family.Sortie !== null) {
consistencyIssues.push({ consistencyIssues.push({
familyId: family.title, familyId: family.Titre,
issueType: family.status + " with endResistant", issueType: family.Statut + " with endResistant",
}); });
} }
} }

View file

@ -1,50 +1,73 @@
import { Client } from "@notionhq/client"; import { Client, isFullPage } from "@notionhq/client";
import { import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
PageObjectResponse, import { Family, FamilyEvent, StatutFamille } from "../../data/Family";
QueryDatabaseParameters,
} from "@notionhq/client/build/src/api-endpoints";
import { Family, FamilyStatus } from "../../data/Family";
import { datePropertyToDate } from "../utils/properties/datePropertyToDate"; 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 { statusPropertyToText } from "../utils/properties/statusPropertyToText";
import { titlePropertyToText } from "../utils/properties/titlePropertyToText copy"; import { titlePropertyToText } from "../utils/properties/titlePropertyToText";
import { queryAllDbResults } from "../utils/queryAllDbResults"; import { queryAllDbResults } from "../utils/queryAllDbResults";
import { assertFullPage } from "../utils/types/assertFullPage"; import { richTextPropertyToPlainText } from "../utils/text/richTextPropertyToPlainText";
export async function fetchFamiliesWithEventsFromNotion( export async function fetchFamiliesWithEventsFromNotion(
notionClient: Client notionClient: Client
): Promise<Family[]> { ): Promise<Family[]> {
const familiesDbId: string = "5b69e02b296d4a578f8c8ab7fe8b05da"; const familiesDbId: string = "5b69e02b296d4a578f8c8ab7fe8b05da";
const dbQuery: QueryDatabaseParameters = { const familEventsDbId: string = "c4d434b4603c4481a4d445618ecdf999";
database_id: familiesDbId,
/*filter: { const eventPages = (
property: "Statut", await queryAllDbResults(notionClient, {
status: { database_id: familEventsDbId,
equals: "Résistant.e", })
}, ).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( const families: Family[] = await Promise.all(
results.map((pageObjectResponse) => { familyPages.map((pageObjectResponse) => {
assertFullPage(pageObjectResponse); return buildFamily(pageObjectResponse, familyEvents);
return buildFamily(pageObjectResponse);
}) })
); );
return families; 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; const pageProperties = page.properties;
// TODO Fetch Family Events
const family: Family = { const family: Family = {
notionId: page.id, notionId: page.id,
title: titlePropertyToText(pageProperties, ""), Titre: titlePropertyToText(pageProperties, ""),
status: statusPropertyToText(pageProperties, "Statut") as FamilyStatus, Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille,
startResistsant: datePropertyToDate(pageProperties, "Intégration"), Integration: datePropertyToDate(pageProperties, "Intégration"),
endResistant: datePropertyToDate(pageProperties, "Sortie"), Sortie: datePropertyToDate(pageProperties, "Sortie"),
evenements: [], Evenements: familyEvents.filter((fe) => fe.notionIdFamille === page.id),
}; };
return family; return family;
} }

View file

@ -41,10 +41,18 @@ export async function publishCurrentStats(
block_id: statsPageId, block_id: statsPageId,
after: currentStatsHeadingBlock.id, after: currentStatsHeadingBlock.id,
children: [ children: [
currentStatBlock("Nb Famille Résistante", stats.resistantsCount), currentStatBlock("Nb Famille Résistante", stats.nbFamilleResistantes),
currentStatBlock( currentStatBlock(
"Nb Famille Résistante ou Ex-Résistante", "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
), ),
], ],
}); });

View file

@ -15,9 +15,9 @@ export async function publishStatisticsToNotion(
) { ) {
await publishCurrentStats(notionClient, stats); 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( async function publishPeriodStats(
@ -50,10 +50,12 @@ async function publishPeriodStats(
}, },
], ],
}, },
"Nb Famille Résistante": numberProp(stat.resistantsCount), "Nb Famille Résistante": numberProp(stat.nbFamilleResistantes),
"Nb Famille Résistante - Evol": numberProp(stat.resistantsCountEvol), "Nb Famille Résistante - Evol": numberProp(
stat.nbFamilleResistantesEvol
),
"Nb Famille Résistante - Evol %": numberProp( "Nb Famille Résistante - Evol %": numberProp(
stat.resistantsCountEvolPercent stat.nbFamilleResistantesEvolPercent
), ),
}, },
}); });

View file

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

View file

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

View file

@ -4,12 +4,15 @@ import { extractPagePropertyValue } from "./extractPagePropertyValue";
export function statusPropertyToText( export function statusPropertyToText(
pageProperties: PageProperties, pageProperties: PageProperties,
propName: string propName: string
): string { ): string | null {
const propValue = extractPagePropertyValue(pageProperties, propName); const propValue = extractPagePropertyValue(pageProperties, propName);
if (propValue.type !== "status") { if (propValue.type !== "status") {
throw new Error( 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; return propValue.status!.name;
} }

View file

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

View file

@ -1,21 +1,19 @@
export type ELStats = { export type ELStats = {
/** Current Resistants Count */ nbFamilleResistantes: number;
resistantsCount: number;
/** Includes Ancient resistants */ /** Includes Ancient resistants */
resistantsOrExCount: number; nbFamilleResistantesOrEx: number;
/** dureeMoyenneResistance: number;
* Resistant count per Year dureeMedianeResistance: number;
* Family Partially resistant over a year are counted in.
*/
years: ELPeriodStats[];
months: ELPeriodStats[]; annees: ELPeriodStats[];
mois: ELPeriodStats[];
}; };
export type ELPeriodStats = { export type ELPeriodStats = {
periodId: string; periodId: string;
resistantsCount: number; nbFamilleResistantes: number;
resistantsCountEvol: number; nbFamilleResistantesEvol: number;
resistantsCountEvolPercent: number; nbFamilleResistantesEvolPercent: number;
}; };

View file

@ -15,14 +15,14 @@ export function computeELPeriodStats(
const stats: ELPeriodStats = { const stats: ELPeriodStats = {
periodId: period.id, periodId: period.id,
nbFamilleResistantes: resistantsCount,
nbFamilleResistantesEvol: evol(
resistantsCount, resistantsCount,
resistantsCountEvol: evol( previousELPeriodStats?.nbFamilleResistantes
resistantsCount,
previousELPeriodStats?.resistantsCount
), ),
resistantsCountEvolPercent: evolPercent( nbFamilleResistantesEvolPercent: evolPercent(
resistantsCount, resistantsCount,
previousELPeriodStats?.resistantsCount previousELPeriodStats?.nbFamilleResistantes
), ),
}; };
periodStats.push(stats); periodStats.push(stats);

View file

@ -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 { ELStats } from "./ELStats";
import { computeELPeriodStats } from "./computeELPeriodStats"; import { computeELPeriodStats } from "./computeELPeriodStats";
import { generateELMonths } from "./generateELMonths"; import { generateELMonths } from "./generateELMonths";
import { generateELYears } from "./generateELYears"; import { generateELYears } from "./generateELYears";
import { average } from "./math/average";
import { median } from "./math/median";
export function computeELStats(families: Family[]): ELStats { export function computeELStats(families: Family[]): ELStats {
const resistantsCount = families.filter(isResistant).length; const resistantsCount = families.filter(isResistant).length;
const resistantsOrExCount = families.filter( const resistantsOrEx = families.filter(
(f) => isResistant(f) || isExResistant(f) (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 elYears = generateELYears();
const yearsStats = computeELPeriodStats(families, elYears); const yearsStats = computeELPeriodStats(families, elYears);
@ -17,9 +30,11 @@ export function computeELStats(families: Family[]): ELStats {
const monthsStats = computeELPeriodStats(families, months); const monthsStats = computeELPeriodStats(families, months);
return { return {
resistantsCount, nbFamilleResistantes: resistantsCount,
resistantsOrExCount, nbFamilleResistantesOrEx: resistantsOrEx.length,
years: yearsStats, dureeMoyenneResistance: dureeMoyenne,
months: monthsStats, dureeMedianeResistance: dureeMediane,
annees: yearsStats,
mois: monthsStats,
}; };
} }

View file

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

View file

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