feat: refactor + ajoute Nb familleMuses en demeure
parent
3de3aa2662
commit
54fcebebf0
|
@ -0,0 +1,101 @@
|
||||||
|
import { differenceInDays } from "date-fns";
|
||||||
|
import { Period } from "../period/Period";
|
||||||
|
import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping";
|
||||||
|
import { isPeriodContaining } from "../period/isPeriodContaining";
|
||||||
|
|
||||||
|
export type Famille = {
|
||||||
|
notionId: string;
|
||||||
|
Titre: string;
|
||||||
|
Statut: StatutFamille;
|
||||||
|
Integration: Date | null;
|
||||||
|
Sortie: Date | null;
|
||||||
|
Evenements: EvenementFamille[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EvenementFamille = {
|
||||||
|
notionId: string;
|
||||||
|
notionIdFamille: string;
|
||||||
|
Évènement: string;
|
||||||
|
Date: Date | null;
|
||||||
|
Type: TypeEvenement;
|
||||||
|
"Enfants concernés": string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TypeEvenement =
|
||||||
|
| "Mise en demeure de scolarisation"
|
||||||
|
| "Composition pénale refusée"
|
||||||
|
| "Composition pénale acceptée";
|
||||||
|
|
||||||
|
export type StatutFamille =
|
||||||
|
| "Résistant.e"
|
||||||
|
| "Ex résistant·e·s"
|
||||||
|
| "À préciser"
|
||||||
|
| "Se questionne"
|
||||||
|
| "Désobéissence décidée"
|
||||||
|
| "Rédaction Déclaration"
|
||||||
|
| "Déclaration Validée - Attente éléments"
|
||||||
|
| "Abdandon"
|
||||||
|
| "Incompatible";
|
||||||
|
|
||||||
|
export function periodOfResistance(
|
||||||
|
family: Famille,
|
||||||
|
atDate: Date = new Date(Date.now())
|
||||||
|
): Period | null {
|
||||||
|
if (family.Statut !== "Résistant.e" && family.Statut !== "Ex résistant·e·s") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!family.Integration || family.Integration.getTime() > atDate.getTime()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start: family.Integration,
|
||||||
|
end:
|
||||||
|
family.Sortie !== null && family.Sortie.getTime() < atDate.getTime()
|
||||||
|
? family.Sortie
|
||||||
|
: atDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResistant(
|
||||||
|
family: Famille,
|
||||||
|
date: Date = new Date(Date.now())
|
||||||
|
): boolean {
|
||||||
|
const por = periodOfResistance(family, date);
|
||||||
|
return por !== null && isPeriodContaining(por, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExResistant(
|
||||||
|
family: Famille,
|
||||||
|
date: Date = new Date(Date.now())
|
||||||
|
): boolean {
|
||||||
|
const por = periodOfResistance(family, date);
|
||||||
|
return por !== null && por.end.getTime() < date.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResistantOverPeriod(
|
||||||
|
family: Famille,
|
||||||
|
period: Period
|
||||||
|
): boolean {
|
||||||
|
const familyPeriodResistant: Period | null = periodOfResistance(family);
|
||||||
|
return (
|
||||||
|
familyPeriodResistant !== null &&
|
||||||
|
arePeriodsOverlaping(familyPeriodResistant, period)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param family
|
||||||
|
* @param atDate
|
||||||
|
* @returns the duration of resistance in days or null if family was not yet in resistances at this date
|
||||||
|
*/
|
||||||
|
export function dureeResistanceInDays(
|
||||||
|
family: Famille,
|
||||||
|
atDate: Date = new Date(Date.now())
|
||||||
|
): number | null {
|
||||||
|
const period = periodOfResistance(family, atDate);
|
||||||
|
if (period == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return differenceInDays(period.end, period.start);
|
||||||
|
}
|
|
@ -1,87 +0,0 @@
|
||||||
import { arePeriodsOverlaping } from "../period/arePeriodsOverlaping";
|
|
||||||
import { Period } from "../period/Period";
|
|
||||||
|
|
||||||
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"
|
|
||||||
| "Se questionne"
|
|
||||||
| "Désobéissence décidée"
|
|
||||||
| "Rédaction Déclaration"
|
|
||||||
| "Déclaration Validée - Attente éléments"
|
|
||||||
| "Abdandon"
|
|
||||||
| "Incompatible";
|
|
||||||
|
|
||||||
export function isResistant(family: Family): boolean {
|
|
||||||
return (
|
|
||||||
family.Statut === "Résistant.e" &&
|
|
||||||
family.Integration !== null &&
|
|
||||||
family.Sortie === null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isExResistant(family: Family): boolean {
|
|
||||||
return (
|
|
||||||
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.Integration!.getTime() <= date.getTime()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isExResistant(family) &&
|
|
||||||
family.Integration!.getTime() <= date.getTime() &&
|
|
||||||
family.Sortie!.getTime() > date.getTime()
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isResistantOverPeriod(family: Family, period: Period): boolean {
|
|
||||||
const familyPeriodResistant: Period | null = periodOfResistance(family);
|
|
||||||
return (
|
|
||||||
familyPeriodResistant !== null &&
|
|
||||||
arePeriodsOverlaping(familyPeriodResistant, period)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function periodOfResistance(family: Family): Period | null {
|
|
||||||
if (isResistant(family)) {
|
|
||||||
const periodResistant: Period = {
|
|
||||||
start: family.Integration!,
|
|
||||||
end: new Date(Date.now()),
|
|
||||||
};
|
|
||||||
return periodResistant;
|
|
||||||
}
|
|
||||||
if (isExResistant(family)) {
|
|
||||||
const periodResistant: Period = {
|
|
||||||
start: family.Integration!,
|
|
||||||
end: family.Sortie!,
|
|
||||||
};
|
|
||||||
return periodResistant;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Family } from "./Family";
|
import { Famille } from "./Famille";
|
||||||
|
|
||||||
export function checkDataConsistency(families: Family[]): ConsistencyIssue[] {
|
export function checkDataConsistency(families: Famille[]): ConsistencyIssue[] {
|
||||||
return families.flatMap((family) => {
|
return families.flatMap((family) => {
|
||||||
return checkFamilyDataConsistency(family);
|
return checkFamilyDataConsistency(family);
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ export type ConsistencyIssue = {
|
||||||
issueType: string;
|
issueType: string;
|
||||||
familyId: string;
|
familyId: string;
|
||||||
};
|
};
|
||||||
function checkFamilyDataConsistency(family: Family) {
|
function checkFamilyDataConsistency(family: Famille) {
|
||||||
const consistencyIssues: ConsistencyIssue[] = [];
|
const consistencyIssues: ConsistencyIssue[] = [];
|
||||||
|
|
||||||
if (family.Statut === "Résistant.e") {
|
if (family.Statut === "Résistant.e") {
|
||||||
|
|
|
@ -30,8 +30,6 @@ import { computeELStats } from "./statistiques/computeELStats";
|
||||||
console.log("Building statistics...");
|
console.log("Building statistics...");
|
||||||
const resistantCountStats = computeELStats(families);
|
const resistantCountStats = computeELStats(families);
|
||||||
|
|
||||||
console.log(resistantCountStats);
|
|
||||||
|
|
||||||
console.log("Publishing statistics...");
|
console.log("Publishing statistics...");
|
||||||
publishStatisticsToNotion(resistantCountStats, notionClient);
|
publishStatisticsToNotion(resistantCountStats, notionClient);
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { Client, isFullPage } from "@notionhq/client";
|
import { Client, isFullPage } from "@notionhq/client";
|
||||||
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
|
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
|
||||||
import { Family, FamilyEvent, StatutFamille } from "../../data/Family";
|
import {
|
||||||
|
EvenementFamille,
|
||||||
|
Famille,
|
||||||
|
StatutFamille,
|
||||||
|
TypeEvenement,
|
||||||
|
} from "../../data/Famille";
|
||||||
import { datePropertyToDate } from "../utils/properties/datePropertyToDate";
|
import { datePropertyToDate } from "../utils/properties/datePropertyToDate";
|
||||||
import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId";
|
import { relationPropertyToPageId } from "../utils/properties/relationPropertyToPageId";
|
||||||
import { selectPropertyToText } from "../utils/properties/selectPropertyToText";
|
import { selectPropertyToText } from "../utils/properties/selectPropertyToText";
|
||||||
|
@ -11,7 +16,7 @@ import { richTextPropertyToPlainText } from "../utils/text/richTextPropertyToPla
|
||||||
|
|
||||||
export async function fetchFamiliesWithEventsFromNotion(
|
export async function fetchFamiliesWithEventsFromNotion(
|
||||||
notionClient: Client
|
notionClient: Client
|
||||||
): Promise<Family[]> {
|
): Promise<Famille[]> {
|
||||||
const familiesDbId: string = "5b69e02b296d4a578f8c8ab7fe8b05da";
|
const familiesDbId: string = "5b69e02b296d4a578f8c8ab7fe8b05da";
|
||||||
const familEventsDbId: string = "c4d434b4603c4481a4d445618ecdf999";
|
const familEventsDbId: string = "c4d434b4603c4481a4d445618ecdf999";
|
||||||
|
|
||||||
|
@ -30,7 +35,7 @@ export async function fetchFamiliesWithEventsFromNotion(
|
||||||
return buildFamilyEvent(pageObjectResponse);
|
return buildFamilyEvent(pageObjectResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
const families: Family[] = await Promise.all(
|
const families: Famille[] = await Promise.all(
|
||||||
familyPages.map((pageObjectResponse) => {
|
familyPages.map((pageObjectResponse) => {
|
||||||
return buildFamily(pageObjectResponse, familyEvents);
|
return buildFamily(pageObjectResponse, familyEvents);
|
||||||
})
|
})
|
||||||
|
@ -38,13 +43,13 @@ export async function fetchFamiliesWithEventsFromNotion(
|
||||||
return families;
|
return families;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFamilyEvent(page: PageObjectResponse): FamilyEvent {
|
function buildFamilyEvent(page: PageObjectResponse): EvenementFamille {
|
||||||
const pageProperties = page.properties;
|
const pageProperties = page.properties;
|
||||||
|
|
||||||
const familyEvent: FamilyEvent = {
|
const familyEvent: EvenementFamille = {
|
||||||
notionId: page.id,
|
notionId: page.id,
|
||||||
Évènement: titlePropertyToText(pageProperties, "Évènement"),
|
Évènement: titlePropertyToText(pageProperties, "Évènement"),
|
||||||
Type: selectPropertyToText(pageProperties, "Type")!,
|
Type: selectPropertyToText(pageProperties, "Type")! as TypeEvenement,
|
||||||
"Enfants concernés": richTextPropertyToPlainText(
|
"Enfants concernés": richTextPropertyToPlainText(
|
||||||
pageProperties,
|
pageProperties,
|
||||||
"Enfants concernés"
|
"Enfants concernés"
|
||||||
|
@ -57,11 +62,11 @@ function buildFamilyEvent(page: PageObjectResponse): FamilyEvent {
|
||||||
|
|
||||||
function buildFamily(
|
function buildFamily(
|
||||||
page: PageObjectResponse,
|
page: PageObjectResponse,
|
||||||
familyEvents: FamilyEvent[]
|
familyEvents: EvenementFamille[]
|
||||||
): Family {
|
): Famille {
|
||||||
const pageProperties = page.properties;
|
const pageProperties = page.properties;
|
||||||
|
|
||||||
const family: Family = {
|
const family: Famille = {
|
||||||
notionId: page.id,
|
notionId: page.id,
|
||||||
Titre: titlePropertyToText(pageProperties, ""),
|
Titre: titlePropertyToText(pageProperties, ""),
|
||||||
Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille,
|
Statut: statusPropertyToText(pageProperties, "Statut") as StatutFamille,
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { Client, isFullPage } from "@notionhq/client";
|
||||||
|
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
|
||||||
|
import { ELPeriodStats, ValueWithEvol } from "../../statistiques/ELStats";
|
||||||
|
import { titlePropertyToText } from "../utils/properties/titlePropertyToText";
|
||||||
|
import { queryAllDbResults } from "../utils/queryAllDbResults";
|
||||||
|
import { removeBlocks } from "../utils/removeBlocks";
|
||||||
|
import { CreatePageProperties } from "../utils/types/CreatePageProperties";
|
||||||
|
import {
|
||||||
|
statNameDureeResistanceMediane,
|
||||||
|
statNameDureeResistanceMoyenne,
|
||||||
|
statsNameNbFamillesMisesEnDemeure,
|
||||||
|
statsNameNbFamillesResistantes,
|
||||||
|
} from "./statNames";
|
||||||
|
|
||||||
|
export async function publishPeriodStats(
|
||||||
|
notionClient: Client,
|
||||||
|
periodStatsDbId: string,
|
||||||
|
stats: ELPeriodStats[]
|
||||||
|
) {
|
||||||
|
const periodRows = (
|
||||||
|
await queryAllDbResults(notionClient, {
|
||||||
|
database_id: periodStatsDbId,
|
||||||
|
})
|
||||||
|
).filter(isFullPage);
|
||||||
|
|
||||||
|
const indexedPeriodRows: { [period: string]: PageObjectResponse } =
|
||||||
|
Object.fromEntries(
|
||||||
|
periodRows.map((r) => [titlePropertyToText(r.properties, "Période"), r])
|
||||||
|
);
|
||||||
|
|
||||||
|
const indexedPeriodStats = Object.fromEntries(
|
||||||
|
stats.map((stat) => [stat.periodId, stat])
|
||||||
|
);
|
||||||
|
|
||||||
|
const rowIdsToDelete = Object.entries(indexedPeriodRows)
|
||||||
|
.filter(
|
||||||
|
([periodId]) =>
|
||||||
|
!Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
|
||||||
|
)
|
||||||
|
.map(([, row]) => row.id);
|
||||||
|
|
||||||
|
const periodIdsToUpdate = Object.entries(indexedPeriodRows)
|
||||||
|
.filter(([periodId]) =>
|
||||||
|
Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
|
||||||
|
)
|
||||||
|
.map(([periodId]) => periodId);
|
||||||
|
|
||||||
|
const periodIdsToCreate = Object.entries(indexedPeriodStats)
|
||||||
|
.filter(
|
||||||
|
([periodId]) =>
|
||||||
|
!Object.prototype.hasOwnProperty.call(indexedPeriodRows, periodId)
|
||||||
|
)
|
||||||
|
.map(([periodId]) => periodId);
|
||||||
|
|
||||||
|
// Delete rows to delte
|
||||||
|
await removeBlocks(notionClient, rowIdsToDelete);
|
||||||
|
|
||||||
|
// Create rows to create
|
||||||
|
for (const periodId of periodIdsToCreate) {
|
||||||
|
const stat = indexedPeriodStats[periodId];
|
||||||
|
await notionClient.pages.create({
|
||||||
|
parent: {
|
||||||
|
database_id: periodStatsDbId,
|
||||||
|
},
|
||||||
|
properties: buildRowPropertiesForUpsert(stat),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update rows
|
||||||
|
for (const periodId of periodIdsToUpdate) {
|
||||||
|
const stat = indexedPeriodStats[periodId];
|
||||||
|
const row = indexedPeriodRows[periodId];
|
||||||
|
await notionClient.pages.update({
|
||||||
|
page_id: row.id,
|
||||||
|
properties: buildRowPropertiesForUpsert(stat),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRowPropertiesForUpsert(
|
||||||
|
stat: ELPeriodStats
|
||||||
|
): CreatePageProperties {
|
||||||
|
return {
|
||||||
|
Période: {
|
||||||
|
title: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: stat.periodId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[statsNameNbFamillesResistantes]: valueWithEvolProp(
|
||||||
|
stat.nbFamilleResistantes
|
||||||
|
),
|
||||||
|
[statNameDureeResistanceMediane]: valueWithEvolProp(
|
||||||
|
stat.dureeResistanceMediane
|
||||||
|
),
|
||||||
|
[statNameDureeResistanceMoyenne]: valueWithEvolProp(
|
||||||
|
stat.dureeResistanceMoyenne
|
||||||
|
),
|
||||||
|
[statsNameNbFamillesMisesEnDemeure]: valueWithEvolProp(
|
||||||
|
stat.nbFamillesMisesEnDemeure
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueWithEvolProp(n: ValueWithEvol) {
|
||||||
|
const formatted = formatValueWithEvol(n);
|
||||||
|
return {
|
||||||
|
rich_text: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: formatted,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function formatValueWithEvol(n: ValueWithEvol): string {
|
||||||
|
const value = n.value.toLocaleString("fr-FR", {
|
||||||
|
useGrouping: false,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
if (isNaN(n.evol)) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
const evol = n.evol.toLocaleString("fr-FR", {
|
||||||
|
useGrouping: false,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
signDisplay: "always",
|
||||||
|
});
|
||||||
|
const evolPercent = Math.round(n.evolPercent).toLocaleString("fr-FR", {
|
||||||
|
useGrouping: false,
|
||||||
|
signDisplay: "always",
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${value} (${evol} | ${evolPercent}%)`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,7 @@
|
||||||
import { Client, isFullPage } from "@notionhq/client";
|
import { Client } from "@notionhq/client";
|
||||||
import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
|
import { ELStats } from "../../statistiques/ELStats";
|
||||||
import {
|
import { publishPeriodStats } from "./publishPeriodStats";
|
||||||
ELPeriodStats,
|
import { publishStatsActuelles } from "./publishStatsActuelles";
|
||||||
ELStats,
|
|
||||||
ValueWithEvol,
|
|
||||||
} from "../../statistiques/ELStats";
|
|
||||||
import { titlePropertyToText } from "../utils/properties/titlePropertyToText";
|
|
||||||
import { queryAllDbResults } from "../utils/queryAllDbResults";
|
|
||||||
import { removeBlocks } from "../utils/removeBlocks";
|
|
||||||
import { publishCurrentStats } from "./publishCurrentStats";
|
|
||||||
|
|
||||||
export const statsPageId = "2b91cd90e3694e96bb196d69aeca59b1";
|
export const statsPageId = "2b91cd90e3694e96bb196d69aeca59b1";
|
||||||
export const currentStatsHeading = "Statistiques actuelles";
|
export const currentStatsHeading = "Statistiques actuelles";
|
||||||
|
@ -19,123 +12,9 @@ export async function publishStatisticsToNotion(
|
||||||
stats: ELStats,
|
stats: ELStats,
|
||||||
notionClient: Client
|
notionClient: Client
|
||||||
) {
|
) {
|
||||||
await publishCurrentStats(notionClient, stats);
|
await publishStatsActuelles(notionClient, stats.actuelles);
|
||||||
|
|
||||||
await publishPeriodStats(notionClient, yearStatsDb, stats.annees);
|
await publishPeriodStats(notionClient, yearStatsDb, stats.annees);
|
||||||
|
|
||||||
await publishPeriodStats(notionClient, monthStatsDb, stats.mois);
|
await publishPeriodStats(notionClient, monthStatsDb, stats.mois);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publishPeriodStats(
|
|
||||||
notionClient: Client,
|
|
||||||
periodStatsDbId: string,
|
|
||||||
stats: ELPeriodStats[]
|
|
||||||
) {
|
|
||||||
const periodRows = (
|
|
||||||
await queryAllDbResults(notionClient, {
|
|
||||||
database_id: periodStatsDbId,
|
|
||||||
})
|
|
||||||
).filter(isFullPage);
|
|
||||||
|
|
||||||
const indexedPeriodRows: { [period: string]: PageObjectResponse } =
|
|
||||||
Object.fromEntries(
|
|
||||||
periodRows.map((r) => [titlePropertyToText(r.properties, "Période"), r])
|
|
||||||
);
|
|
||||||
|
|
||||||
const indexedPeriodStats = Object.fromEntries(
|
|
||||||
stats.map((stat) => [stat.periodId, stat])
|
|
||||||
);
|
|
||||||
|
|
||||||
const rowIdsToDelete = Object.entries(indexedPeriodRows)
|
|
||||||
.filter(
|
|
||||||
([periodId]) =>
|
|
||||||
!Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
|
|
||||||
)
|
|
||||||
.map(([, row]) => row.id);
|
|
||||||
|
|
||||||
const periodIdsToUpdate = Object.entries(indexedPeriodRows)
|
|
||||||
.filter(([periodId]) =>
|
|
||||||
Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
|
|
||||||
)
|
|
||||||
.map(([periodId]) => periodId);
|
|
||||||
|
|
||||||
const periodIdsToCreate = Object.entries(indexedPeriodStats)
|
|
||||||
.filter(
|
|
||||||
([periodId]) =>
|
|
||||||
!Object.prototype.hasOwnProperty.call(indexedPeriodRows, periodId)
|
|
||||||
)
|
|
||||||
.map(([periodId]) => periodId);
|
|
||||||
|
|
||||||
// Delete rows to delte
|
|
||||||
await removeBlocks(notionClient, rowIdsToDelete);
|
|
||||||
|
|
||||||
// Create rows to create
|
|
||||||
for (const periodId of periodIdsToCreate) {
|
|
||||||
const stat = indexedPeriodStats[periodId];
|
|
||||||
await notionClient.pages.create({
|
|
||||||
parent: {
|
|
||||||
database_id: periodStatsDbId,
|
|
||||||
},
|
|
||||||
properties: buildRowPropertiesForUpsert(stat),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update rows
|
|
||||||
for (const periodId of periodIdsToUpdate) {
|
|
||||||
const stat = indexedPeriodStats[periodId];
|
|
||||||
const row = indexedPeriodRows[periodId];
|
|
||||||
await notionClient.pages.update({
|
|
||||||
page_id: row.id,
|
|
||||||
properties: buildRowPropertiesForUpsert(stat),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRowPropertiesForUpsert(stat: ELPeriodStats) {
|
|
||||||
return {
|
|
||||||
Période: {
|
|
||||||
title: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: stat.periodId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"Nb Famille Résistante": valueWithEvolProp(stat.nbFamilleResistantes),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function valueWithEvolProp(n: ValueWithEvol) {
|
|
||||||
const formatted = formatValueWithEvol(n);
|
|
||||||
return {
|
|
||||||
rich_text: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: formatted,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function formatValueWithEvol(n: ValueWithEvol): string {
|
|
||||||
const value = n.value.toLocaleString("fr-FR", {
|
|
||||||
useGrouping: false,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
if (isNaN(n.evol)) {
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
const evol = n.evol.toLocaleString("fr-FR", {
|
|
||||||
useGrouping: false,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
signDisplay: "always",
|
|
||||||
});
|
|
||||||
const evolPercent = Math.round(n.evolPercent).toLocaleString("fr-FR", {
|
|
||||||
useGrouping: false,
|
|
||||||
signDisplay: "always",
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${value} (${evol} | ${evolPercent}%)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
import { Client, isFullBlock } from "@notionhq/client";
|
import { Client, isFullBlock } from "@notionhq/client";
|
||||||
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
|
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
|
||||||
import { ELStats } from "../../statistiques/ELStats";
|
import { ELStatsActuelles } from "../../statistiques/ELStats";
|
||||||
import { listAllChildrenBlocks } from "../utils/listAllChildrenBlocks";
|
import { listAllChildrenBlocks } from "../utils/listAllChildrenBlocks";
|
||||||
import { removeBlocks } from "../utils/removeBlocks";
|
import { removeBlocks } from "../utils/removeBlocks";
|
||||||
import { richTextToPlainText } from "../utils/text/richTextToPlainText";
|
import { richTextToPlainText } from "../utils/text/richTextToPlainText";
|
||||||
import { currentStatsHeading, statsPageId } from "./publishStatisticsToNotion";
|
import { currentStatsHeading, statsPageId } from "./publishStatisticsToNotion";
|
||||||
|
import {
|
||||||
|
statNameDureeResistanceMediane,
|
||||||
|
statNameDureeResistanceMoyenne,
|
||||||
|
statsNameNbFamillesMisesEnDemeure,
|
||||||
|
statsNameNbFamillesResistantes,
|
||||||
|
statsNameNbFamillesResistantesOuEx,
|
||||||
|
statsNamePourcentageFamilleMisesEnDemeure,
|
||||||
|
} from "./statNames";
|
||||||
|
|
||||||
export async function publishCurrentStats(
|
export async function publishStatsActuelles(
|
||||||
notionClient: Client,
|
notionClient: Client,
|
||||||
stats: ELStats
|
statsActuelles: ELStatsActuelles
|
||||||
) {
|
) {
|
||||||
const childrenBlocks = (
|
const childrenBlocks = (
|
||||||
await listAllChildrenBlocks(notionClient, {
|
await listAllChildrenBlocks(notionClient, {
|
||||||
|
@ -41,18 +49,29 @@ export async function publishCurrentStats(
|
||||||
block_id: statsPageId,
|
block_id: statsPageId,
|
||||||
after: currentStatsHeadingBlock.id,
|
after: currentStatsHeadingBlock.id,
|
||||||
children: [
|
children: [
|
||||||
currentStatBlock("Nb Famille Résistante", stats.nbFamilleResistantes),
|
|
||||||
currentStatBlock(
|
currentStatBlock(
|
||||||
"Nb Famille Résistante ou Ex-Résistante",
|
statsNameNbFamillesResistantes,
|
||||||
stats.nbFamilleResistantesOrEx
|
statsActuelles.nbFamilleResistantes
|
||||||
),
|
),
|
||||||
currentStatBlock(
|
currentStatBlock(
|
||||||
"Durée Moyenne Résistance (jours)",
|
statsNameNbFamillesResistantesOuEx,
|
||||||
stats.dureeMoyenneResistance
|
statsActuelles.nbFamilleResistantesOrEx
|
||||||
),
|
),
|
||||||
currentStatBlock(
|
currentStatBlock(
|
||||||
"Durée Médiane Résistance (jours)",
|
statNameDureeResistanceMoyenne,
|
||||||
stats.dureeMedianeResistance
|
statsActuelles.dureeResistanceMoyenne
|
||||||
|
),
|
||||||
|
currentStatBlock(
|
||||||
|
statNameDureeResistanceMediane,
|
||||||
|
statsActuelles.dureeResistanceMediane
|
||||||
|
),
|
||||||
|
currentStatBlock(
|
||||||
|
statsNameNbFamillesMisesEnDemeure,
|
||||||
|
statsActuelles.nbFamillesMiseEnDemeure
|
||||||
|
),
|
||||||
|
currentStatBlock(
|
||||||
|
statsNamePourcentageFamilleMisesEnDemeure,
|
||||||
|
statsActuelles.pourcentageFamillesMisesEnDemeure
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const statNameDureeResistanceMoyenne = "Durée Résistance Moyenne";
|
||||||
|
export const statNameDureeResistanceMediane = "Durée Résistance Médiane";
|
||||||
|
export const statsNameNbFamillesResistantes = "Nb Familles Résistantes";
|
||||||
|
export const statsNameNbFamillesResistantesOuEx =
|
||||||
|
"Nb Familles Résistante ou Ex-Résistantes";
|
||||||
|
export const statsNameNbFamillesMisesEnDemeure = "Nb Familles Mises en Demeure";
|
||||||
|
export const statsNamePourcentageFamilleMisesEnDemeure =
|
||||||
|
"Pourcentage de Familles Mises en Demeure";
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { CreatePageParameters } from "@notionhq/client/build/src/api-endpoints";
|
||||||
|
|
||||||
|
export type CreatePageProperties = CreatePageParameters["properties"];
|
|
@ -1,5 +1,4 @@
|
||||||
export type Period = {
|
export type Period = {
|
||||||
start: Date;
|
start: Date;
|
||||||
/** Exclusive */
|
|
||||||
end: Date;
|
end: Date;
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe("isPeriodContaining", () => {
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("period does not contain end date", () => {
|
test("period contains end date", () => {
|
||||||
expect(
|
expect(
|
||||||
isPeriodContaining(
|
isPeriodContaining(
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@ describe("isPeriodContaining", () => {
|
||||||
},
|
},
|
||||||
new Date(Date.UTC(2024, 3, 1))
|
new Date(Date.UTC(2024, 3, 1))
|
||||||
)
|
)
|
||||||
).toBe(false);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
test("period does not contain date before", () => {
|
test("period does not contain date before", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|
|
@ -7,7 +7,7 @@ export function isPeriodContaining(
|
||||||
if (dateOrPeriod instanceof Date) {
|
if (dateOrPeriod instanceof Date) {
|
||||||
return (
|
return (
|
||||||
period.start.getTime() <= dateOrPeriod.getTime() &&
|
period.start.getTime() <= dateOrPeriod.getTime() &&
|
||||||
dateOrPeriod.getTime() < period.end.getTime()
|
dateOrPeriod.getTime() <= period.end.getTime()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
export type ELStats = {
|
export type ELStats = {
|
||||||
|
actuelles: ELStatsActuelles;
|
||||||
|
annees: ELPeriodStats[];
|
||||||
|
|
||||||
|
mois: ELPeriodStats[];
|
||||||
|
};
|
||||||
|
export type ELStatsActuelles = {
|
||||||
nbFamilleResistantes: number;
|
nbFamilleResistantes: number;
|
||||||
/** Includes Ancient resistants */
|
/** Includes Ancient resistants */
|
||||||
nbFamilleResistantesOrEx: number;
|
nbFamilleResistantesOrEx: number;
|
||||||
|
|
||||||
dureeMoyenneResistance: number;
|
dureeResistanceMoyenne: number;
|
||||||
dureeMedianeResistance: number;
|
dureeResistanceMediane: number;
|
||||||
|
nbFamillesMiseEnDemeure: number;
|
||||||
annees: ELPeriodStats[];
|
pourcentageFamillesMisesEnDemeure: number;
|
||||||
|
|
||||||
mois: ELPeriodStats[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ELPeriodStats = {
|
export type ELPeriodStats = {
|
||||||
periodId: string;
|
periodId: string;
|
||||||
nbFamilleResistantes: ValueWithEvol;
|
nbFamilleResistantes: ValueWithEvol;
|
||||||
|
dureeResistanceMoyenne: ValueWithEvol;
|
||||||
|
dureeResistanceMediane: ValueWithEvol;
|
||||||
|
nbFamillesMisesEnDemeure: ValueWithEvol;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ValueWithEvol = {
|
export type ValueWithEvol = {
|
||||||
|
|
|
@ -1,24 +1,59 @@
|
||||||
import { Family, isResistantOverPeriod } from "../data/Family";
|
import {
|
||||||
|
Famille,
|
||||||
|
dureeResistanceInDays,
|
||||||
|
isResistantOverPeriod,
|
||||||
|
} from "../data/Famille";
|
||||||
import { IdentifiedPeriod } from "../period/IdentifiedPeriod";
|
import { IdentifiedPeriod } from "../period/IdentifiedPeriod";
|
||||||
|
import { isPeriodContaining } from "../period/isPeriodContaining";
|
||||||
import { ELPeriodStats, ValueWithEvol } from "./ELStats";
|
import { ELPeriodStats, ValueWithEvol } from "./ELStats";
|
||||||
|
import { average } from "./math/average";
|
||||||
|
import { median } from "./math/median";
|
||||||
|
|
||||||
export function computeELPeriodStats(
|
export function computeELPeriodStats(
|
||||||
familles: Family[],
|
familles: Famille[],
|
||||||
periods: IdentifiedPeriod[]
|
periods: IdentifiedPeriod[]
|
||||||
): ELPeriodStats[] {
|
): ELPeriodStats[] {
|
||||||
const periodStats: ELPeriodStats[] = [];
|
const periodStats: ELPeriodStats[] = [];
|
||||||
let previousELPeriodStats: ELPeriodStats | null = null;
|
let previousELPeriodStats: ELPeriodStats | null = null;
|
||||||
for (const period of periods) {
|
for (const period of periods) {
|
||||||
const resistantsCount = familles.filter((famille) =>
|
const nbFamilleResistantes = familles.filter((famille) =>
|
||||||
isResistantOverPeriod(famille, period)
|
isResistantOverPeriod(famille, period)
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
const periodEndOrNow =
|
||||||
|
period.end.getTime() > Date.now() ? new Date(Date.now()) : period.end;
|
||||||
|
|
||||||
|
const dureesResistances = familles
|
||||||
|
.map((famille) => dureeResistanceInDays(famille, periodEndOrNow))
|
||||||
|
.filter(notNull);
|
||||||
|
const dureeResistanceMediane = Math.round(median(dureesResistances));
|
||||||
|
const dureeResistanceMoyenne = Math.round(average(dureesResistances));
|
||||||
|
const nbFamillesMiseEnDemeure = familles.filter((famille) =>
|
||||||
|
famille.Evenements.find(
|
||||||
|
(e) =>
|
||||||
|
e.Type === "Mise en demeure de scolarisation" &&
|
||||||
|
e.Date &&
|
||||||
|
isPeriodContaining(period, e.Date)
|
||||||
|
)
|
||||||
|
).length;
|
||||||
const stats: ELPeriodStats = {
|
const stats: ELPeriodStats = {
|
||||||
periodId: period.id,
|
periodId: period.id,
|
||||||
nbFamilleResistantes: valueWithEvol(
|
nbFamilleResistantes: valueWithEvol(
|
||||||
resistantsCount,
|
nbFamilleResistantes,
|
||||||
previousELPeriodStats?.nbFamilleResistantes.value
|
previousELPeriodStats?.nbFamilleResistantes.value
|
||||||
),
|
),
|
||||||
|
dureeResistanceMediane: valueWithEvol(
|
||||||
|
dureeResistanceMediane,
|
||||||
|
previousELPeriodStats?.dureeResistanceMediane.value
|
||||||
|
),
|
||||||
|
dureeResistanceMoyenne: valueWithEvol(
|
||||||
|
dureeResistanceMoyenne,
|
||||||
|
previousELPeriodStats?.dureeResistanceMoyenne.value
|
||||||
|
),
|
||||||
|
nbFamillesMisesEnDemeure: valueWithEvol(
|
||||||
|
nbFamillesMiseEnDemeure,
|
||||||
|
previousELPeriodStats?.nbFamillesMisesEnDemeure.value
|
||||||
|
),
|
||||||
};
|
};
|
||||||
periodStats.push(stats);
|
periodStats.push(stats);
|
||||||
previousELPeriodStats = stats;
|
previousELPeriodStats = stats;
|
||||||
|
@ -46,3 +81,9 @@ function evol(current: number, previous: number | undefined): number {
|
||||||
if (previous === undefined) return NaN;
|
if (previous === undefined) return NaN;
|
||||||
return current - previous;
|
return current - previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Typescript is not able to infer this inline see
|
||||||
|
// https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array
|
||||||
|
function notNull<TValue>(value: TValue | null): value is TValue {
|
||||||
|
return value !== null;
|
||||||
|
}
|
||||||
|
|
|
@ -1,28 +1,12 @@
|
||||||
import {
|
import { Famille } from "../data/Famille";
|
||||||
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 { computeStatsActuelles } from "./computeStatsActuelles";
|
||||||
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 {
|
|
||||||
const resistantsCount = families.filter(isResistant).length;
|
|
||||||
const resistantsOrEx = families.filter(
|
|
||||||
(f) => isResistant(f) || isExResistant(f)
|
|
||||||
);
|
|
||||||
const durations = resistantsOrEx.map((f) =>
|
|
||||||
daysInPeriod(periodOfResistance(f)!)
|
|
||||||
);
|
|
||||||
const dureeMoyenne = average(durations);
|
|
||||||
const dureeMediane = median(durations);
|
|
||||||
|
|
||||||
|
export function computeELStats(families: Famille[]): ELStats {
|
||||||
|
const actuelles = computeStatsActuelles(families);
|
||||||
const elYears = generateELYears();
|
const elYears = generateELYears();
|
||||||
const yearsStats = computeELPeriodStats(families, elYears);
|
const yearsStats = computeELPeriodStats(families, elYears);
|
||||||
|
|
||||||
|
@ -30,10 +14,7 @@ export function computeELStats(families: Family[]): ELStats {
|
||||||
const monthsStats = computeELPeriodStats(families, months);
|
const monthsStats = computeELPeriodStats(families, months);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nbFamilleResistantes: resistantsCount,
|
actuelles: actuelles,
|
||||||
nbFamilleResistantesOrEx: resistantsOrEx.length,
|
|
||||||
dureeMoyenneResistance: dureeMoyenne,
|
|
||||||
dureeMedianeResistance: dureeMediane,
|
|
||||||
annees: yearsStats,
|
annees: yearsStats,
|
||||||
mois: monthsStats,
|
mois: monthsStats,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
Famille,
|
||||||
|
isExResistant,
|
||||||
|
isResistant,
|
||||||
|
periodOfResistance,
|
||||||
|
} from "../data/Famille";
|
||||||
|
import { daysInPeriod } from "../period/daysInPeriod";
|
||||||
|
import { ELStatsActuelles } from "./ELStats";
|
||||||
|
import { average } from "./math/average";
|
||||||
|
import { median } from "./math/median";
|
||||||
|
|
||||||
|
export function computeStatsActuelles(familles: Famille[]): ELStatsActuelles {
|
||||||
|
const resistantsCount = familles.filter((f) => isResistant(f)).length;
|
||||||
|
const resistantsOrEx = familles.filter(
|
||||||
|
(f) => isResistant(f) || isExResistant(f)
|
||||||
|
);
|
||||||
|
const durations = resistantsOrEx.map((f) =>
|
||||||
|
daysInPeriod(periodOfResistance(f)!)
|
||||||
|
);
|
||||||
|
const dureeMoyenne = average(durations);
|
||||||
|
const dureeMediane = median(durations);
|
||||||
|
|
||||||
|
const familleAvecMiseEnDemeure = resistantsOrEx.filter((f) =>
|
||||||
|
f.Evenements.find((e) => e.Type === "Mise en demeure de scolarisation")
|
||||||
|
);
|
||||||
|
|
||||||
|
const actuelles: ELStatsActuelles = {
|
||||||
|
nbFamilleResistantes: resistantsCount,
|
||||||
|
nbFamilleResistantesOrEx: resistantsOrEx.length,
|
||||||
|
dureeResistanceMoyenne: dureeMoyenne,
|
||||||
|
dureeResistanceMediane: dureeMediane,
|
||||||
|
nbFamillesMiseEnDemeure: familleAvecMiseEnDemeure.length,
|
||||||
|
pourcentageFamillesMisesEnDemeure:
|
||||||
|
(100 * familleAvecMiseEnDemeure.length) / resistantsOrEx.length,
|
||||||
|
};
|
||||||
|
return actuelles;
|
||||||
|
}
|
Loading…
Reference in New Issue