Skip to Content
Statistiques et Rapports Admin

Statistiques et Rapports Admin

Ce document décrit le module apps/api/src/stats (StatsModule), qui calcule les données consommées par la page administrateur /reports du dashboard (apps/dashboard/app/(dashboard)/reports) : chiffre d’affaires, statuts de commandes, meilleures ventes, moyens de paiement et croissance des utilisateurs, sur une fenêtre glissante configurable.


1. Vue d’ensemble

Le StatsModule (apps/api/src/stats) expose deux endpoints derrière un seul contrôleur :

  1. GET /stats/dashboard : indicateurs adaptés au rôle de l’utilisateur connecté (Admin, Vendeur, Acheteur) — c’est le tableau de bord d’accueil, distinct de la page administrateur /reports.
  2. GET /stats/reports : indicateurs agrégés sur une période paramétrable, réservés à l’administrateur — c’est l’endpoint qui alimente la page /reports.

Contrairement au module d’audit, il n’y a ici aucune écriture : StatsService lit et agrège des données déjà stockées par les autres modules (Order, OrderItem, Payment, User). Aucune table dédiée aux statistiques n’existe — tout est calculé à la volée à chaque requête, sans matérialisation.


2. Ce que calcule getReports(rangeDays)

Le cœur du module est StatsService.getReports() (stats.service.ts). Il accepte une fenêtre en jours (rangeDays) et calcule, pour cette période et pour la période précédente de même durée (utilisée comme point de comparaison) :

A. Indicateurs clés avec évolution (kpis)

IndicateurCalcul
revenueSomme des totalAmount de toutes les commandes créées dans la période dont le statut n’est pas CANCELLED.
orderCountNombre total de commandes créées dans la période (y compris les commandes annulées).
avgOrderValuerevenue / nombre de commandes non annulées (0 si aucune).
newUsersNombre de comptes User créés dans la période (tous rôles confondus).

Chaque indicateur est accompagné d’un xxxChangePct : variation en pourcentage arrondie par rapport à la période précédente de même durée (pctChange). Cas particulier : si la valeur précédente est 0, le pourcentage retourné est 100 si la valeur courante est positive, sinon null (variation non calculable, affichée comme ”—” côté dashboard).

B. Séries temporelles jour par jour

  • revenueSeries : chiffre d’affaires (commandes non annulées) regroupé par jour calendaire (toISOString().slice(0, 10)), sur toute la période demandée, y compris les jours à 0.
  • userGrowthSeries : nombre de nouvelles inscriptions par jour, même construction.

C. Répartition des statuts de commande (orderStatusBreakdown)

Comptage des commandes de la période par OrderStatus (PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED), avec pourcentage de chaque statut sur le total de commandes de la période (y compris les annulées, contrairement au calcul du chiffre d’affaires).

D. Meilleures ventes par produit (topProducts)

Calculé à partir des OrderItem des commandes non annulées de la période : chiffre d’affaires par produit (quantity × unitPrice, cumulé), trié par CA décroissant, limité aux 5 premiers.

E. Répartition des moyens de paiement (paymentMethodBreakdown)

Comptage des Payment créés dans la période, groupés par nom de PaymentMethod ('Inconnu' si la méthode est introuvable), avec pourcentage sur le total de paiements de la période.

[!NOTE] Le comptage des moyens de paiement porte sur les tentatives de paiement enregistrées (Payment.createdAt dans la période), pas uniquement sur celles au statut PAID — le service ne filtre pas payments par status. Un paiement FAILED ou PENDING compte donc de la même façon qu’un paiement PAID dans cette répartition.


3. Multi-pays et multi-devise : agrégation sans conversion

Le schéma Prisma montre que Order porte un currencyCode figé (dérivé du pays de l’adresse de livraison au moment de la commande), et que Country définit des vatRate/shippingFee propres à chaque pays. StatsService.getReports() a été relu spécifiquement pour vérifier si l’agrégation en tient compte.

[!WARNING] StatsService ne filtre ni ne groupe par Country ou currencyCode, et n’effectue aucune conversion de devise. Le calcul de revenue, avgOrderValue, revenueSeries et topProducts fait une simple somme arithmétique de Number(totalAmount) sur toutes les commandes de la période, quel que soit leur currencyCode. Si la marketplace opère sur plusieurs pays avec des devises différentes (XAF, EUR, USD, CAD, GBP — cf. le convertisseur de devise du Store), les montants agrégés mélangent silencieusement des unités monétaires différentes sous un seul total libellé “FCFA” côté dashboard (voir §4). Ce comportement n’est vraisemblablement correct que tant que toutes les commandes sont passées dans une seule devise en pratique ; il devient trompeur dès que plusieurs pays à devises distinctes génèrent des commandes simultanément. Aucun filtre par pays n’est exposé, ni côté API (/stats/reports n’accepte que range) ni côté UI.


4. Rôles et Autorisations (RBAC)

Le contrôleur (stats.controller.ts) applique des gardes différents selon l’endpoint :

EndpointMéthodeRôle requisDescription
/stats/dashboardGETAuthentifié (tous rôles)Indicateurs adaptés au rôle courant (Admin/Vendeur/Acheteur) — tableau de bord d’accueil.
/stats/reportsGETADMINIndicateurs agrégés de la page /reports (@UseGuards(RolesGuard) + @Roles(UserRole.ADMIN) en plus du contrôle JWT global du contrôleur).

Le contrôleur porte @UseGuards(JwtAuthGuard) au niveau de la classe (authentification obligatoire pour tout), et ajoute RolesGuard + @Roles(ADMIN) spécifiquement sur getReports. Côté dashboard, apps/dashboard/app/(dashboard)/reports/page.tsx fait la même double vérification que la page Journal d’activité : décodage du JWT, redirection vers / si payload.role !== 'ADMIN'.


5. Interface d’administration (dashboard)

Page : apps/dashboard/app/(dashboard)/reports/page.tsx.

  • Sélecteur de période (ReportsRangeSelector.tsx) : 7 / 30 / 90 jours, reflété dans l’URL (?range=). Le contrôleur API borne strictement range à [7, 30, 90] (défaut 30 si valeur absente ou invalide).
  • Bandeau d’indicateurs : chiffre d’affaires, commandes, panier moyen, nouveaux utilisateurs — chacun avec un badge de variation (▲/▼ + pourcentage vs période précédente, ou ”—” si non calculable).
  • Graphique CA dans le temps : SVG fait main (pas de librairie de charts), courbe + aire, à partir de revenueSeries.
  • Répartition des statuts de commande : barre segmentée + légende, aux couleurs par statut (DELIVERED vert, CANCELLED rouge, etc.), triée dans un ordre métier fixe (DELIVERED, SHIPPED, CONFIRMED, PENDING, CANCELLED) plutôt que par volume.
  • Meilleures ventes : barres horizontales proportionnelles au CA du top 5 produits.
  • Moyens de paiement : barres de progression + pourcentage par méthode.
  • Croissance des utilisateurs : second graphique SVG à partir de userGrowthSeries.
  • Export CSV (ExportCsvButton.tsx) : génère et télécharge côté client (sans appel serveur additionnel) un CSV multi-sections (résumé, série CA/jour, statuts, top produits, moyens de paiement) à partir des données déjà chargées par la page. Nom de fichier : rapport-{rangeDays}j-{date}.csv.

[!NOTE] Tous les montants sont affichés en FCFA (formatage toLocaleString('fr-FR') + suffixe FCFA codé en dur dans l’UI), cohérent avec la limite décrite en §3 : le dashboard part du principe qu’un seul référentiel monétaire est en jeu.


6. Guide des Endpoints et Exemples

Statistiques agrégées (GET /stats/reports)

  • Accès : Admin uniquement (Authorization: Bearer <token_admin>).
  • Requête :
    GET /stats/reports?range=30
  • Réponse (200 OK) :
    { "rangeDays": 30, "kpis": { "revenue": 4250000, "revenueChangePct": 12, "orderCount": 87, "orderCountChangePct": -4, "avgOrderValue": 54487, "avgOrderValueChangePct": 16, "newUsers": 23, "newUsersChangePct": 100 }, "revenueSeries": [ { "day": "2026-08-30", "value": 120000 }, { "day": "2026-08-31", "value": 0 } ], "userGrowthSeries": [ { "day": "2026-08-30", "value": 2 }, { "day": "2026-08-31", "value": 0 } ], "orderStatusBreakdown": [ { "status": "DELIVERED", "count": 52, "pct": 60 }, { "status": "PENDING", "count": 15, "pct": 17 }, { "status": "CANCELLED", "count": 8, "pct": 9 } ], "topProducts": [ { "productId": "7bf3b934-8c81-42ab-ba0c-99d9b62fb91e", "name": "MacBook Pro 14 M3", "revenue": 998000 } ], "paymentMethodBreakdown": [ { "name": "Orange Money", "count": 40, "pct": 46 }, { "name": "PayPal", "count": 30, "pct": 34 } ] }
  • Erreurs : range en dehors de {7, 30, 90} retombe silencieusement sur 30 (pas de 400). Un jeton non-admin reçoit 403 Forbidden (RolesGuard).

Tableau de bord contextuel (GET /stats/dashboard)

  • Accès : tout utilisateur authentifié — la forme de la réponse dépend de role (ADMIN, SELLER ou BUYER), calculée par branche dans getDashboardStats(userId, role). Distinct de l’endpoint /stats/reports (même contrôleur, même module).

7. Comportements à surveiller

  • Pas de conversion/filtrage multi-devise (voir §3) — élément important pour interpréter correctement les chiffres de chiffre d’affaires si la marketplace opère sur plusieurs pays à devises différentes.
  • Chaque chargement de la page /reports relance l’intégralité des requêtes Prisma (plusieurs findMany en parallèle via Promise.all), sans cache ni matérialisation.
  • Seules les fenêtres 7/30/90 jours sont supportées ; pas de plage de dates personnalisée.
  • La répartition des moyens de paiement (§2.E) inclut les tentatives échouées et en attente, pas seulement les paiements confirmés.
Last updated on