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 DemandesRecentes, DashboardDemandeDto? PremiereAValider, IReadOnlyList FacturesEchues, IReadOnlyList AvoirsDisponibles, IReadOnlyList Echeances);