From 1437e59b7f4330ea828c16cb530812bd76052fb2 Mon Sep 17 00:00:00 2001 From: Xantass Date: Fri, 27 Jun 2025 10:03:09 +0200 Subject: [PATCH 1/5] feat: add simple controller and service --- src/modules/kpi/kpi.controller.ts | 48 ++++++++++++++++ src/modules/kpi/kpi.service.ts | 94 +++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/src/modules/kpi/kpi.controller.ts b/src/modules/kpi/kpi.controller.ts index 9befad2..6d1c0e4 100644 --- a/src/modules/kpi/kpi.controller.ts +++ b/src/modules/kpi/kpi.controller.ts @@ -291,4 +291,52 @@ export class KpiController { throw new InternalServerErrorException('Server error'); } } + + /** + * Compte le nombre de commandes dans un intervalle donné, avec possibilité de regroupement par tranche de minutes. + * @param idRestaurant - L'identifiant du restaurant (doit être positif) + * @param timeBegin - Date/heure de début de l'intervalle (obligatoire) + * @param timeEnd - Date/heure de fin de l'intervalle (obligatoire) + * @param breakdown - (optionnel) Durée de la tranche en minutes pour le regroupement + * @returns Soit le nombre total de commandes, soit un objet groupé par tranche de temps + * @throws {BadRequestException} Si les paramètres sont invalides + * @throws {InternalServerErrorException} En cas d'erreur serveur + * @example + * GET /api/1/kpi/ordersCount?timeBegin=2024-01-01T00:00:00Z&timeEnd=2024-01-01T01:00:00Z&breakdown=15 + * // retourne { "00:00": 5, "00:15": 8, ... } + * GET /api/1/kpi/ordersCount?timeBegin=2024-01-01T00:00:00Z&timeEnd=2024-01-01T01:00:00Z + * // retourne 23 + */ + @Get('ordersCount') + async kpiOrdersCount( + @Param('idRestaurant', PositiveNumberPipe) idRestaurant: number, + @Query('timeBegin', DatePipe) timeBegin: string, + @Query('timeEnd', DatePipe) timeEnd: string, + @Query('breakdown') breakdown?: string, + ) { + try { + if (breakdown) { + // On attend un nombre entier positif pour breakdown (minutes) + const breakdownMinutes = parseInt(breakdown, 10); + if (isNaN(breakdownMinutes) || breakdownMinutes <= 0) { + throw new BadRequestException('Le paramètre breakdown doit être un entier positif (minutes)'); + } + return await this.kpiService.ordersCountGrouped( + idRestaurant, + timeBegin, + timeEnd, + breakdownMinutes, + ); + } else { + return await this.kpiService.ordersCount( + idRestaurant, + timeBegin, + timeEnd, + ); + } + } catch (error) { + if (error instanceof BadRequestException) throw error; + throw new InternalServerErrorException('Erreur serveur'); + } + } } diff --git a/src/modules/kpi/kpi.service.ts b/src/modules/kpi/kpi.service.ts index 6c1631f..83da0e2 100644 --- a/src/modules/kpi/kpi.service.ts +++ b/src/modules/kpi/kpi.service.ts @@ -526,4 +526,98 @@ export class KpiService extends DB { return { 'Average value': average, 'Nbr orders': orders.length }; } + + /** + * Retourne le nombre total de commandes pour un restaurant dans un intervalle donné. + * @param idRestaurant - L'identifiant du restaurant + * @param timeBegin - Date/heure de début de l'intervalle (obligatoire) + * @param timeEnd - Date/heure de fin de l'intervalle (obligatoire) + * @returns Le nombre total de commandes + */ + async ordersCount( + idRestaurant: number, + timeBegin: string, + timeEnd: string, + ): Promise { + const db = this.getDbConnection(); + let orders = await db + .collection('restaurant') + .aggregate([ + { $match: { id: idRestaurant } }, + { $unwind: '$orders' }, + { + $project: { + 'orders.date': 1, + _id: 0, + }, + }, + ]) + .toArray(); + + if (timeBegin && timeEnd) { + const beginDate = new Date(timeBegin); + const endDate = new Date(timeEnd); + orders = orders.filter((item) => { + const orderDate = new Date(item.orders.date); + return orderDate >= beginDate && orderDate <= endDate; + }); + } + return orders.length; + } + + /** + * Retourne le nombre de commandes groupées par tranche de minutes à partir de timeBegin jusqu'à timeEnd. + * @param idRestaurant - L'identifiant du restaurant + * @param timeBegin - Date/heure de début de l'intervalle (obligatoire) + * @param timeEnd - Date/heure de fin de l'intervalle (obligatoire) + * @param breakdownMinutes - Durée de la tranche en minutes + * @returns Un objet dont les clés sont les heures de début de tranche (HH:mm) et les valeurs le nombre de commandes dans la tranche + */ + async ordersCountGrouped( + idRestaurant: number, + timeBegin: string, + timeEnd: string, + breakdownMinutes: number, + ): Promise> { + const db = this.getDbConnection(); + let orders = await db + .collection('restaurant') + .aggregate([ + { $match: { id: idRestaurant } }, + { $unwind: '$orders' }, + { + $project: { + 'orders.date': 1, + _id: 0, + }, + }, + ]) + .toArray(); + + const beginDate = new Date(timeBegin); + const endDate = new Date(timeEnd); + orders = orders.filter((item) => { + const orderDate = new Date(item.orders.date); + return orderDate >= beginDate && orderDate <= endDate; + }); + + // Préparer les tranches + const result: Record = {}; + let current = new Date(beginDate); + while (current < endDate) { + const next = new Date(current.getTime() + breakdownMinutes * 60000); + // Format HH:mm + const label = current.toISOString().substr(11, 5); + result[label] = 0; + // Compter les commandes dans la tranche + orders.forEach((item) => { + const orderDate = new Date(item.orders.date); + if (orderDate >= current && orderDate < next) { + result[label]++; + } + }); + current = next; + } + return result; + } } From 23f9b9841721c40febde5ccd4989f34159d186a9 Mon Sep 17 00:00:00 2001 From: Xantass Date: Fri, 27 Jun 2025 23:58:23 +0200 Subject: [PATCH 2/5] feat: add new route for get data of order send --- src/modules/kpi/kpi.controller.ts | 49 +++++++++------- src/modules/kpi/kpi.service.ts | 94 ------------------------------- 2 files changed, 28 insertions(+), 115 deletions(-) diff --git a/src/modules/kpi/kpi.controller.ts b/src/modules/kpi/kpi.controller.ts index 6d1c0e4..4407371 100644 --- a/src/modules/kpi/kpi.controller.ts +++ b/src/modules/kpi/kpi.controller.ts @@ -312,31 +312,38 @@ export class KpiController { @Param('idRestaurant', PositiveNumberPipe) idRestaurant: number, @Query('timeBegin', DatePipe) timeBegin: string, @Query('timeEnd', DatePipe) timeEnd: string, - @Query('breakdown') breakdown?: string, + @Query('breakdown') breakdown?: number, ) { - try { - if (breakdown) { - // On attend un nombre entier positif pour breakdown (minutes) - const breakdownMinutes = parseInt(breakdown, 10); - if (isNaN(breakdownMinutes) || breakdownMinutes <= 0) { - throw new BadRequestException('Le paramètre breakdown doit être un entier positif (minutes)'); - } - return await this.kpiService.ordersCountGrouped( - idRestaurant, - timeBegin, - timeEnd, - breakdownMinutes, - ); - } else { - return await this.kpiService.ordersCount( + if (breakdown) { + const slots: { timeBegin: string; timeEnd: string }[] = []; + let current = new Date(timeBegin); + const end = new Date(timeEnd); + const orders = {}; + + while (current < end) { + const slotStart = new Date(current); + const slotEnd = new Date(current.getTime() + breakdown * 60000); + slots.push({ + timeBegin: slotStart.toISOString(), + timeEnd: (slotEnd < end ? slotEnd : end).toISOString(), + }); + current = slotEnd; + } + for (const slot of slots) { + const date = new Date(slot.timeBegin); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + orders[`${hours}:${minutes}`] = await this.kpiService.clientsCount( idRestaurant, - timeBegin, - timeEnd, + slot.timeBegin, + slot.timeEnd, + undefined, + undefined ); } - } catch (error) { - if (error instanceof BadRequestException) throw error; - throw new InternalServerErrorException('Erreur serveur'); + return orders; + } else { + return this.kpiService.clientsCount(idRestaurant, timeBegin, timeEnd, undefined, undefined); } } } diff --git a/src/modules/kpi/kpi.service.ts b/src/modules/kpi/kpi.service.ts index 83da0e2..6c1631f 100644 --- a/src/modules/kpi/kpi.service.ts +++ b/src/modules/kpi/kpi.service.ts @@ -526,98 +526,4 @@ export class KpiService extends DB { return { 'Average value': average, 'Nbr orders': orders.length }; } - - /** - * Retourne le nombre total de commandes pour un restaurant dans un intervalle donné. - * @param idRestaurant - L'identifiant du restaurant - * @param timeBegin - Date/heure de début de l'intervalle (obligatoire) - * @param timeEnd - Date/heure de fin de l'intervalle (obligatoire) - * @returns Le nombre total de commandes - */ - async ordersCount( - idRestaurant: number, - timeBegin: string, - timeEnd: string, - ): Promise { - const db = this.getDbConnection(); - let orders = await db - .collection('restaurant') - .aggregate([ - { $match: { id: idRestaurant } }, - { $unwind: '$orders' }, - { - $project: { - 'orders.date': 1, - _id: 0, - }, - }, - ]) - .toArray(); - - if (timeBegin && timeEnd) { - const beginDate = new Date(timeBegin); - const endDate = new Date(timeEnd); - orders = orders.filter((item) => { - const orderDate = new Date(item.orders.date); - return orderDate >= beginDate && orderDate <= endDate; - }); - } - return orders.length; - } - - /** - * Retourne le nombre de commandes groupées par tranche de minutes à partir de timeBegin jusqu'à timeEnd. - * @param idRestaurant - L'identifiant du restaurant - * @param timeBegin - Date/heure de début de l'intervalle (obligatoire) - * @param timeEnd - Date/heure de fin de l'intervalle (obligatoire) - * @param breakdownMinutes - Durée de la tranche en minutes - * @returns Un objet dont les clés sont les heures de début de tranche (HH:mm) et les valeurs le nombre de commandes dans la tranche - */ - async ordersCountGrouped( - idRestaurant: number, - timeBegin: string, - timeEnd: string, - breakdownMinutes: number, - ): Promise> { - const db = this.getDbConnection(); - let orders = await db - .collection('restaurant') - .aggregate([ - { $match: { id: idRestaurant } }, - { $unwind: '$orders' }, - { - $project: { - 'orders.date': 1, - _id: 0, - }, - }, - ]) - .toArray(); - - const beginDate = new Date(timeBegin); - const endDate = new Date(timeEnd); - orders = orders.filter((item) => { - const orderDate = new Date(item.orders.date); - return orderDate >= beginDate && orderDate <= endDate; - }); - - // Préparer les tranches - const result: Record = {}; - let current = new Date(beginDate); - while (current < endDate) { - const next = new Date(current.getTime() + breakdownMinutes * 60000); - // Format HH:mm - const label = current.toISOString().substr(11, 5); - result[label] = 0; - // Compter les commandes dans la tranche - orders.forEach((item) => { - const orderDate = new Date(item.orders.date); - if (orderDate >= current && orderDate < next) { - result[label]++; - } - }); - current = next; - } - return result; - } } From 1df8b1a9486bca8cdc46b0512bcce3c3ff102fde Mon Sep 17 00:00:00 2001 From: Xantass Date: Sat, 28 Jun 2025 12:15:31 +0200 Subject: [PATCH 3/5] doc: translate documentation french to english --- src/modules/kpi/kpi.controller.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/modules/kpi/kpi.controller.ts b/src/modules/kpi/kpi.controller.ts index 4407371..b4c3297 100644 --- a/src/modules/kpi/kpi.controller.ts +++ b/src/modules/kpi/kpi.controller.ts @@ -293,19 +293,19 @@ export class KpiController { } /** - * Compte le nombre de commandes dans un intervalle donné, avec possibilité de regroupement par tranche de minutes. - * @param idRestaurant - L'identifiant du restaurant (doit être positif) - * @param timeBegin - Date/heure de début de l'intervalle (obligatoire) - * @param timeEnd - Date/heure de fin de l'intervalle (obligatoire) - * @param breakdown - (optionnel) Durée de la tranche en minutes pour le regroupement - * @returns Soit le nombre total de commandes, soit un objet groupé par tranche de temps - * @throws {BadRequestException} Si les paramètres sont invalides - * @throws {InternalServerErrorException} En cas d'erreur serveur + * Counts the number of orders within a given interval, with optional grouping by time slots (in minutes). + * @param idRestaurant - The restaurant identifier (must be positive) + * @param timeBegin - Start date/time of the interval (required) + * @param timeEnd - End date/time of the interval (required) + * @param breakdown - (optional) Slot duration in minutes for grouping + * @returns Either the total number of orders, or an object grouped by time slot + * @throws {BadRequestException} If parameters are invalid + * @throws {InternalServerErrorException} In case of server error * @example * GET /api/1/kpi/ordersCount?timeBegin=2024-01-01T00:00:00Z&timeEnd=2024-01-01T01:00:00Z&breakdown=15 - * // retourne { "00:00": 5, "00:15": 8, ... } + * // returns { "00:00": 5, "00:15": 8, ... } * GET /api/1/kpi/ordersCount?timeBegin=2024-01-01T00:00:00Z&timeEnd=2024-01-01T01:00:00Z - * // retourne 23 + * // returns 23 */ @Get('ordersCount') async kpiOrdersCount( From a621c4bf5817ec819aeef1a5cae6a817de4b57a2 Mon Sep 17 00:00:00 2001 From: Xantass Date: Sat, 28 Jun 2025 12:19:57 +0200 Subject: [PATCH 4/5] docs: add route into swagger --- docs/swagger.json | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/swagger.json b/docs/swagger.json index 7765b93..c5ec0ab 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3312,6 +3312,63 @@ } ] } + }, + "/{restaurant_id}/kpi/ordersCount": { + "get": { + "tags": ["KPI"], + "summary": "Count the number of orders in a given interval, with optional grouping by time slot.", + "parameters": [ + { + "name": "restaurant_id", + "in": "path", + "description": "ID of the restaurant", + "required": true, + "schema": { "type": "integer" } + }, + { + "name": "timeBegin", + "in": "query", + "description": "Start date/time of the interval (ISO 8601)", + "required": true, + "schema": { "type": "string", "format": "date-time" } + }, + { + "name": "timeEnd", + "in": "query", + "description": "End date/time of the interval (ISO 8601)", + "required": true, + "schema": { "type": "string", "format": "date-time" } + }, + { + "name": "breakdown", + "in": "query", + "description": "Slot duration in minutes for grouping (optional)", + "required": false, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "Returns either the total number of orders, or an object grouped by time slot.", + "content": { + "application/json": { + "examples": { + "Grouped": { + "value": { "00:00": 5, "00:15": 8 } + }, + "Total": { + "value": 23 + } + } + } + } + }, + "400": { "description": "Invalid parameters" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "5XX": { "$ref": "#/components/responses/ServerError" } + }, + "security": [{ "BearerAuth": [] }] + } } }, "components": { From 8b8a7265051a25d8bcfa7625259713b30d4a5e2d Mon Sep 17 00:00:00 2001 From: Xantas Date: Sat, 28 Jun 2025 12:24:05 +0200 Subject: [PATCH 5/5] linter: fix errors --- src/modules/kpi/kpi.controller.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/modules/kpi/kpi.controller.ts b/src/modules/kpi/kpi.controller.ts index db85745..047f84a 100644 --- a/src/modules/kpi/kpi.controller.ts +++ b/src/modules/kpi/kpi.controller.ts @@ -487,12 +487,18 @@ export class KpiController { slot.timeBegin, slot.timeEnd, undefined, - undefined + undefined, ); } return orders; } else { - return this.kpiService.clientsCount(idRestaurant, timeBegin, timeEnd, undefined, undefined); + return this.kpiService.clientsCount( + idRestaurant, + timeBegin, + timeEnd, + undefined, + undefined, + ); } } }