Files
2026-09-11 14:55:41 +02:00

127 lines
6.0 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using SIFinance.Shared.Entities;
namespace SIFinance.Modules.Achats.Endpoints;
// Endpoint agrégé du tableau de bord : tous les compteurs et sommes sont calculés
// par SQL Server (COUNT/SUM), et seules les quelques lignes réellement affichées
// sont transférées. Le temps de réponse ne dépend donc plus du volume en base.
public static class DashboardEndpoints
{
private static readonly string[] StatutsEnAttente = ["SOUMISE", "EN_VALIDATION", "COMPLEMENT_REQUIS"];
private static readonly string[] StatutsEngages = ["VALIDEE", "FACTUREE"];
public static IEndpointRouteBuilder MapDashboardEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/dashboard").WithTags("Dashboard").RequireAuthorization();
group.MapGet("", async (SIFinanceDbContext db, CancellationToken ct) =>
{
var today = DateTime.UtcNow.Date;
// DateEcheance est un DateOnly côté entité : les comparaisons doivent se
// faire avec un DateOnly pour être traduites en SQL.
var todayDate = DateOnly.FromDateTime(today);
// Bornes du trimestre courant et du précédent, calculées côté C# pour que
// la comparaison reste une simple comparaison de dates traduisible en SQL.
var trimCourant = (today.Month - 1) / 3;
var debutTrimestre = new DateTime(today.Year, trimCourant * 3 + 1, 1);
var debutTrimestrePrecedent = debutTrimestre.AddMonths(-3);
var demandes = db.DemandesAchats.AsNoTracking();
var factures = db.Factures.AsNoTracking();
// --- KPI demandes ---
var enAttente = demandes.Where(d => StatutsEnAttente.Contains(d.Statut));
var nbAValider = await enAttente.CountAsync(ct);
var montantAValider = await enAttente.SumAsync(d => (decimal?)d.MontantTtc, ct) ?? 0m;
var engagees = demandes.Where(d => StatutsEngages.Contains(d.Statut));
var engageCourant = await engagees
.Where(d => d.DateCreation >= debutTrimestre)
.SumAsync(d => (decimal?)d.MontantTtc, ct) ?? 0m;
var engagePrecedent = await engagees
.Where(d => d.DateCreation >= debutTrimestrePrecedent && d.DateCreation < debutTrimestre)
.SumAsync(d => (decimal?)d.MontantTtc, ct) ?? 0m;
// --- KPI factures ---
var aPayer = factures.Where(f => f.Statut == "validee");
var nbAPayer = await aPayer.CountAsync(ct);
var montantAPayer = await aPayer.SumAsync(f => (decimal?)f.MontantTtc, ct) ?? 0m;
var enCours = factures.Where(f => f.Statut != "payee");
var nbEchues = await enCours.CountAsync(f => f.DateEcheance < todayDate, ct);
var nbProches = await enCours.CountAsync(f => f.DateEcheance >= todayDate && f.DateEcheance <= todayDate.AddDays(7), ct);
// --- Listes affichées (volumes bornés par Take) ---
var demandesRecentes = await demandes
.OrderByDescending(d => d.DateCreation)
.Take(4)
.Select(d => new DashboardDemandeDto(d.Id, d.Reference, d.Objet, d.MontantTtc, d.Statut, d.TypeDepenseId))
.ToListAsync(ct);
var demandesAValiderApercu = await enAttente
.OrderByDescending(d => d.DateCreation)
.Take(1)
.Select(d => new DashboardDemandeDto(d.Id, d.Reference, d.Objet, d.MontantTtc, d.Statut, d.TypeDepenseId))
.ToListAsync(ct);
var echeances = await enCours
.OrderBy(f => f.DateEcheance)
.Take(5)
.Select(f => new DashboardEcheanceDto(f.Id, f.NumeroFacture, f.Fournisseur.RaisonSociale, f.MontantTtc, f.DateEcheance))
.ToListAsync(ct);
// Alertes : uniquement les premières lignes réellement affichées.
var facturesEchues = await enCours
.Where(f => f.DateEcheance < todayDate)
.OrderBy(f => f.DateEcheance)
.Take(5)
.Select(f => new DashboardEcheanceDto(f.Id, f.NumeroFacture, f.Fournisseur.RaisonSociale, f.MontantTtc, f.DateEcheance))
.ToListAsync(ct);
var avoirsDisponibles = await db.Avoirs
.AsNoTracking()
.Where(a => a.SoldeDisponible > 0)
.OrderByDescending(a => a.DateCreation)
.Take(5)
.Select(a => new DashboardAvoirDto(a.Id, a.NumeroAvoir, a.Fournisseur.RaisonSociale, a.SoldeDisponible))
.ToListAsync(ct);
return Results.Ok(new DashboardDto(
engageCourant, engagePrecedent,
nbAValider, montantAValider,
nbAPayer, montantAPayer,
nbProches, nbEchues,
demandesRecentes,
demandesAValiderApercu.FirstOrDefault(),
facturesEchues,
avoirsDisponibles,
echeances));
});
return app;
}
}
public record DashboardDemandeDto(int Id, string Reference, string Objet, decimal MontantTtc, string Statut, int TypeDepenseId);
public record DashboardEcheanceDto(int Id, string Numero, string Fournisseur, decimal MontantTtc, DateOnly DateEcheance);
public record DashboardAvoirDto(int Id, string Numero, string Fournisseur, decimal SoldeDisponible);
public record DashboardDto(
decimal EngageCourant,
decimal EngagePrecedent,
int NbAValider,
decimal MontantAValider,
int NbAPayer,
decimal MontantAPayer,
int NbEcheancesProches,
int NbFacturesEchues,
IReadOnlyList<DashboardDemandeDto> DemandesRecentes,
DashboardDemandeDto? PremiereAValider,
IReadOnlyList<DashboardEcheanceDto> FacturesEchues,
IReadOnlyList<DashboardAvoirDto> AvoirsDisponibles,
IReadOnlyList<DashboardEcheanceDto> Echeances);