127 lines
6.0 KiB
C#
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); |