<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\ProduitRepository;
use App\Repository\VenteRepository;
use App\Repository\DepenseRepository;
class DashboardController extends AbstractController
{
/**
* @Route("/", name="app_dashboard")
*/
public function index(
VenteRepository $venteRepo,
ProduitRepository $produitRepo,
DepenseRepository $depenseRepo
): Response {
$this->denyAccessUnlessGranted('VOIR_DASHBOARD');
// --- KPIs VENTES / STOCK ---
$ventesMois = $venteRepo->sumThisMonth();
$margeMois = $venteRepo->margeThisMonth();
$kpis = [
'ventesJour' => $venteRepo->sumToday(),
'ventesMois' => $ventesMois,
'margeJour' => $venteRepo->margeToday(),
'margeMois' => $margeMois,
'tauxMargeMois' => $ventesMois > 0 ? ($margeMois / $ventesMois) * 100 : 0,
'produitsCritiques' => $produitRepo->countCritiques(),
'valeurStock' => $produitRepo->valeurStock(),
];
// Rotation sur 30 jours — méthode COGS (normes gestion de stock)
// Taux = COGS / Stock moyen au prix d'achat
// Stock moyen = (Stock début + Stock fin) / 2
// Stock début estimé = Stock fin + COGS → Stock moyen = Stock fin + COGS / 2
$fromRot = new \DateTime('-30 days');
$toRot = new \DateTime();
$cogs30 = $venteRepo->coutVentesBetween($fromRot, $toRot);
$stockFinValeur = $produitRepo->valeurStockAuPrixAchat();
$stockMoyen = $stockFinValeur + ($cogs30 / 2.0);
$rotation = ($stockMoyen > 0) ? $cogs30 / $stockMoyen : 0;
$delaiRotation = ($rotation > 0) ? round(30 / $rotation) : 0;
$kpis['rotationStock30j'] = $rotation;
$kpis['delaiRotation30j'] = $delaiRotation;
// --- KPIs DÉPENSES ---
$kpis['depensesJour'] = $depenseRepo->sumToday();
$kpis['depensesMois'] = $depenseRepo->sumThisMonth();
// Résultats (très simple : ventes - dépenses)
$kpis['resultatJour'] = $kpis['ventesJour'] - $kpis['depensesJour'];
$kpis['resultatMois'] = $kpis['ventesMois'] - $kpis['depensesMois'];
// --- Graphiques ventes ---
$graphDailySales = $venteRepo->dailySalesLast30Days();
$graphDailyMargins = $venteRepo->dailyMarginsLast30Days();
$monthlySales = $venteRepo->monthlySalesLast12Months();
$forecastMonthly = $this->forecastNextMonths($monthlySales, 3);
// --- Graphiques dépenses ---
$graphDailyExpenses = $depenseRepo->dailyExpensesLast30Days();
$monthlyExpenses = $depenseRepo->monthlyExpensesLast12Months();
$expenseByType = $depenseRepo->sumByType(null, null);
// --- Combinaison ventes / dépenses mensuelles ---
$combinedMonthly = $this->mergeMonthlySalesAndExpenses($monthlySales, $monthlyExpenses);
// --- Tops produits ---
$topQty = $venteRepo->topProductsByQuantity(10);
$topMargin = $venteRepo->topProductsByMargin(10);
// --- Performance par catégorie (30 jours) ---
$perfCategories = $venteRepo->performanceByCategory($fromRot, $toRot);
return $this->render('dashboard/index.html.twig', [
'kpis' => $kpis,
'graphDailySales' => $graphDailySales,
'graphDailyMargins'=> $graphDailyMargins,
'graphDailyExpenses' => $graphDailyExpenses,
'monthlySales' => $monthlySales,
'monthlyExpenses' => $monthlyExpenses,
'combinedMonthly' => $combinedMonthly,
'forecastMonthly' => $forecastMonthly,
'topQty' => $topQty,
'topMargin' => $topMargin,
'perfCategories' => $perfCategories,
'expenseByType' => $expenseByType,
]);
}
/**
* Prévision simple : moyenne des 3 derniers mois pour les N prochains.
*
* @param array $monthlySales tableau issu de monthlySalesLast12Months()
* @param int $nb nombre de mois à prévoir
*/
private function forecastNextMonths(array $monthlySales, int $nb): array
{
if (empty($monthlySales)) {
return [];
}
// Exclure le mois en cours (incomplet — fausserait la moyenne)
$currentMonth = (new \DateTime())->format('Y-m');
$completed = array_values(array_filter($monthlySales, function ($row) use ($currentMonth) {
$key = ($row['mois'] instanceof \DateTimeInterface)
? $row['mois']->format('Y-m')
: substr((string) $row['mois'], 0, 7);
return $key < $currentMonth;
}));
if (empty($completed)) {
return [];
}
// Moyenne glissante sur les 3 derniers mois complets
$last = array_slice($completed, -3);
$count = count($last);
$totals = array_map(fn($r) => (float) ($r['total'] ?? 0), $last);
$avg = array_sum($totals) / $count;
// Tendance linéaire simple (pente moyenne entre les mois utilisés)
$trend = $count >= 2 ? ($totals[$count - 1] - $totals[0]) / ($count - 1) : 0;
// Les prévisions démarrent toujours au mois suivant le mois actuel
$dt = new \DateTime('first day of next month');
$forecasts = [];
for ($i = 0; $i < $nb; $i++) {
$future = (clone $dt)->modify('+' . $i . ' month');
$forecasts[] = [
'mois' => new \DateTime($future->format('Y-m-01')),
'total' => max(0.0, $avg + $trend * ($i + 1)),
'base' => $count,
'trend' => $trend,
];
}
return $forecasts;
}
/**
* Fusionne les séries mensuelles ventes / dépenses sur une même base "YYYY-MM"
*/
private function mergeMonthlySalesAndExpenses(array $sales, array $expenses): array
{
$map = [];
// Ventes
foreach ($sales as $row) {
$rawMonth = isset($row['mois']) ? $row['mois'] : null;
if ($rawMonth instanceof \DateTimeInterface) {
$key = $rawMonth->format('Y-m');
} else {
$key = (string) $rawMonth;
}
if (!isset($map[$key])) {
$map[$key] = ['mois' => $key, 'ventes' => 0.0, 'depenses' => 0.0];
}
$map[$key]['ventes'] = isset($row['total']) ? (float) $row['total'] : 0.0;
}
// Dépenses
foreach ($expenses as $row) {
$rawMonth = isset($row['mois']) ? $row['mois'] : null;
if ($rawMonth instanceof \DateTimeInterface) {
$key = $rawMonth->format('Y-m');
} else {
$key = (string) $rawMonth;
}
if (!isset($map[$key])) {
$map[$key] = ['mois' => $key, 'ventes' => 0.0, 'depenses' => 0.0];
}
$map[$key]['depenses'] = isset($row['total']) ? (float) $row['total'] : 0.0;
}
ksort($map);
return array_values($map);
}
}