Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/stats api endpoint #320

Merged
merged 11 commits into from
Jan 14, 2025
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@sentry/nextjs": "7.120.1",
"@splidejs/react-splide": "^0.7.12",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"html-to-image": "^1.11.11",
"jspdf": "^2.5.2",
"leaflet": "^1.9.4",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions src/app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from "next/server";
import z from "zod";
import { add } from "date-fns/add";
import { startOfYear } from "date-fns/startOfYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfWeek } from "date-fns/startOfWeek";
import { startOfDay } from "date-fns/startOfDay";
import { getNorthStarStats } from "@/src/lib/prisma/prisma-analytics-queries";
import { fr } from "date-fns/locale/fr";

const StatsRouteSchema = z.object({
since: z
.number()
.positive()
.max(5000, { message: "Veuillez rentrer une valeur inférieure à 5000 pour le paramètre since" }),
periodicity: z.enum(["year", "month", "week", "day"]),
});

interface StatOutputRecord {
value: number;
date: Date;
}

type StatOutput = {
description?: string;
stats: StatOutputRecord[];
};

export async function GET(request: NextRequest) {
const parsedRequest = StatsRouteSchema.safeParse({
since: +(request.nextUrl.searchParams.get("since") ?? 0),
periodicity: request.nextUrl.searchParams.get("periodicity"),
});
if (!parsedRequest.success) {
const { errors } = parsedRequest.error;
return NextResponse.json({ error: { message: "Invalid request", errors } }, { status: 400 });
} else {
const { since: nbIntervals, periodicity } = parsedRequest.data;

const ranges = {
year: startOfYear,
month: startOfMonth,
week: (date: Date) => startOfWeek(date, { weekStartsOn: 1, locale: fr }),
day: startOfDay,
};

let dateBeginOfLastPeriod = new Date();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 On pourrait même extraire cette logique dans les helpers de date et avoir un truc du type :

const calculatePeriodDates(periodicity: Periodicity, nbIntervals: number) => {
  let dateBeginOfLastPeriod = new Date();
  
  const periodicityMap = {
    year: () => startOfYear(new Date()),
    month: () => startOfMonth(new Date()),
    week: () => startOfWeek(new Date(), { weekStartsOn: 1, locale: fr }),
    day: () => startOfDay(new Date()),
  };
  ...
}  

dateBeginOfLastPeriod = ranges[periodicity](new Date());
dateBeginOfLastPeriod = add(dateBeginOfLastPeriod, {
minutes: -dateBeginOfLastPeriod.getTimezoneOffset(),
});

const dateBeginOfFirstPeriod = add(dateBeginOfLastPeriod, {
...(periodicity === "year" && { years: 1 - nbIntervals }),
...(periodicity === "month" && { months: 1 - nbIntervals }),
...(periodicity === "week" && { weeks: 1 - nbIntervals }),
...(periodicity === "day" && { days: 1 - nbIntervals }),
});

const results = await getNorthStarStats({ dateFrom: dateBeginOfFirstPeriod, range: periodicity });

const sanitizeResults: StatOutput = {
stats: results.map((result) => ({
value: Number(result.score),
date: result.periode!,
})),
};

return NextResponse.json(sanitizeResults);
}
}
2 changes: 2 additions & 0 deletions src/helpers/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type DateRange = "day" | "week" | "month" | "year";

export const FAR_FUTURE = new Date(3024, 0, 0, 1);

export const removeDaysToDate = (date: Date, nbDays: number) => new Date(date.getTime() - nbDays * 24 * 60 * 60 * 1000);
Expand Down
41 changes: 40 additions & 1 deletion src/lib/prisma/prisma-analytics-queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Analytics } from "@prisma/client";
/* eslint-disable max-len */

import { Analytics, Prisma } from "@prisma/client";
import { prismaClient } from "./prismaClient";
import { DateRange } from "@/src/helpers/dateUtils";

type AnalyticsProps = Omit<Analytics, "id" | "created_at" | "created_by">;

Expand All @@ -14,3 +17,39 @@ export const createAnalytic = async (analytics: AnalyticsProps): Promise<Analyti
},
});
};

type GetNorthStarStatsProps = {
dateFrom: Date;
range: DateRange;
};

export const getNorthStarStats = async (
params: GetNorthStarStatsProps,
): Promise<{ periode: Date; score: number }[]> => {
return prismaClient.$queryRaw(
Prisma.sql`with score_table as (
select
date_trunc(${params.range}, p.created_at) as date1,
count(distinct p.id) as score
from pfmv."projet" p
join pfmv."user_projet" up on p.id = up.projet_id
join pfmv."User" u on up.user_id = u.id
WHERE p.created_at >= ${params.dateFrom}
and p.deleted_at IS NULL
and u.email NOT LIKE '%@ademe.fr'
and u.email NOT LIKE '%@beta.gouv.fr'
and u.email != 'marie.racine@dihal.gouv.fr'
group by 1
),
all_intervals as (
SELECT date_trunc(${params.range}, ts) as date1
FROM generate_series(${params.dateFrom}, now(), CONCAT('1 ', ${params.range})::interval) AS ts
)
select
all_intervals.date1::timestamp::date as "periode",
coalesce(score, 0) as "score"
from score_table
right outer join all_intervals on all_intervals.date1 = score_table.date1
order by 1;`,
);
};
Loading