src/Controller/DashboardController.php line 25

  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Habitat;
  4. use App\Entity\Incident;
  5. use App\Entity\Location;
  6. use App\Entity\Log;
  7. use App\Entity\Payment;
  8. use App\Entity\Reservation;
  9. use App\Entity\User;
  10. use App\Repository\SubscriptionRepository;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use Doctrine\Persistence\ManagerRegistry;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\RequestStack;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\Routing\Annotation\Route;
  18. #[Route('/admin/dashboard')]
  19. class DashboardController extends AbstractController
  20. {
  21.     #[Route('/'name'app_dashboard')]
  22.     public function index(RequestStack $requestStackSubscriptionRepository $subscriptionRepositoryEntityManagerInterface $entityManager): Response {
  23.         $user $this->getUser();
  24.         if (!$user instanceof User) {
  25.             return $this->redirectToRoute('app_logout');
  26.         }
  27.         $this->syncExpiredSubscriptions($subscriptionRepository$entityManager);
  28.         $show $requestStack->getSession()->get('dashboard-overview''long_term');
  29.         $subscriptionRenewalData $this->resolveSubscriptionRenewalCardData($user$subscriptionRepository);
  30.         return $this->render('dashboard/index.html.twig'array_merge([
  31.             'user' => $user,
  32.             'show' => $show
  33.         ], $subscriptionRenewalData));
  34.     }
  35.     #[Route('/view/{show}'name'start.dashboard.toggle.view'methods: ['GET'])]
  36.     public function indexActionToggle(RequestStack $requestStackstring $showManagerRegistry $doctrine): Response
  37.     {
  38.         $em $doctrine->getManager();
  39.         $authUser $this->getUser();
  40.         if (!$authUser) {
  41.             return $this->redirectToRoute('app_logout');
  42.         }
  43.         if ('long_term' === $show) {
  44.             $requestStack->getSession()->set('dashboard-overview''long_term');
  45.             $authUser->setMode('long_term');
  46.         } else {
  47.             $requestStack->getSession()->set('dashboard-overview''short_term');
  48.             $authUser->setMode('short_term');
  49.         }
  50.         $em->flush();
  51.         return $this->forward('App\Controller\DashboardController::index');
  52.     }
  53.     /**
  54.      * Gets the reservation overview.
  55.      * @throws \JsonException
  56.      */
  57.     #[Route('/mode'name'dashboard.get.mode'methods: ['GET'])]
  58.     public function getModeAction(
  59.         ManagerRegistry $doctrine,
  60.         RequestStack $requestStack,
  61.         Request $request,
  62.         SubscriptionRepository $subscriptionRepository
  63.     ): Response
  64.     {
  65.         $mode $request->query->get('mode''long_term');
  66.         if ('long_term' === $mode) {
  67.             return $this->_handleLongTermRequest($doctrine$requestStack$request$subscriptionRepository);
  68.         } else {
  69.             return $this->_handleShortTermRequest($doctrine$requestStack$request$subscriptionRepository);
  70.         }
  71.     }
  72.     /**
  73.      * Displays the regular Long Term overview based on a user long term mode.
  74.      */
  75.     private function _handleLongTermRequest(
  76.         ManagerRegistry $doctrine,
  77.         RequestStack $requestStack,
  78.         Request $request,
  79.         SubscriptionRepository $subscriptionRepository
  80.     ): Response {
  81.         $em $doctrine->getManager();
  82.         $user $this->getUser();
  83.         if (!$user instanceof User) {
  84.             return $this->redirectToRoute('app_logout');
  85.         }
  86.         $entreprise $user->getEntreprise();
  87.         if (!$entreprise || !$entreprise->getId()) {
  88.             return $this->redirectToRoute('app_logout');
  89.         }
  90.         if (!$user->getMode()) {
  91.             $user->setMode('light');
  92.             $em->flush();
  93.         }
  94.         $requestStack->getSession()->remove('location-details-overview');
  95.         $now = new \DateTimeImmutable();
  96.         $currentYear = (int) $now->format('Y');
  97.         $previousYear $currentYear 1;
  98.         $lastTwoMonthDate $now->modify('-2 months');
  99.         $lastOneMonthDate $now->modify('-1 month');
  100.         $lastTwoMonth = (int) $lastTwoMonthDate->format('n');
  101.         $lastOneMonth = (int) $lastOneMonthDate->format('n');
  102.         $yearLastTwo = (int) $lastTwoMonthDate->format('Y');
  103.         $yearLastOne = (int) $lastOneMonthDate->format('Y');
  104.         $entrepriseId = (int) $entreprise->getId();
  105.         $paymentRepo $em->getRepository(Payment::class);
  106.         // Invoice counts grouped by paid status (single query).
  107.         $invoiceRows $em->createQueryBuilder()
  108.             ->select('p.paid AS paidStatus, COUNT(p.id) AS total')
  109.             ->from(Payment::class, 'p')
  110.             ->join('p.marchand''m')
  111.             ->andWhere('m.id = :marchand')
  112.             ->setParameter('marchand'$entrepriseId)
  113.             ->groupBy('p.paid')
  114.             ->getQuery()
  115.             ->getArrayResult();
  116.         $invoicePaidCount 0;
  117.         $invoiceUnpaidCount 0;
  118.         foreach ($invoiceRows as $row) {
  119.             $isPaid filter_var((string) ($row['paidStatus'] ?? ''), FILTER_VALIDATE_BOOLEANFILTER_NULL_ON_FAILURE);
  120.             if ($isPaid === null) {
  121.                 $isPaid = ((int) ($row['paidStatus'] ?? 0)) === 1;
  122.             }
  123.             if ($isPaid) {
  124.                 $invoicePaidCount = (int) ($row['total'] ?? 0);
  125.             } else {
  126.                 $invoiceUnpaidCount = (int) ($row['total'] ?? 0);
  127.             }
  128.         }
  129.         // Active leases count.
  130.         $totalLoyers = (int) $em->createQueryBuilder()
  131.             ->select('COUNT(l.id)')
  132.             ->from(Location::class, 'l')
  133.             ->join('l.marchand''m')
  134.             ->andWhere('m.id = :marchand')
  135.             ->andWhere('l.occuper = :occupied')
  136.             ->setParameter('marchand'$entrepriseId)
  137.             ->setParameter('occupied'true)
  138.             ->getQuery()
  139.             ->getSingleScalarResult();
  140.         // Habitat occupation counts grouped (single query).
  141.         $habitatRows $em->createQueryBuilder()
  142.             ->select('h.occuper AS occupiedStatus, COUNT(h.id) AS total')
  143.             ->from(Habitat::class, 'h')
  144.             ->join('h.marchand''m')
  145.             ->andWhere('m.id = :marchand')
  146.             ->setParameter('marchand'$entrepriseId)
  147.             ->groupBy('h.occuper')
  148.             ->getQuery()
  149.             ->getArrayResult();
  150.         $habitatEnBail 0;
  151.         $habitatLibre 0;
  152.         foreach ($habitatRows as $row) {
  153.             $isOccupied filter_var((string) ($row['occupiedStatus'] ?? ''), FILTER_VALIDATE_BOOLEANFILTER_NULL_ON_FAILURE);
  154.             if ($isOccupied === null) {
  155.                 $isOccupied = ((int) ($row['occupiedStatus'] ?? 0)) === 1;
  156.             }
  157.             if ($isOccupied) {
  158.                 $habitatEnBail = (int) ($row['total'] ?? 0);
  159.             } else {
  160.                 $habitatLibre = (int) ($row['total'] ?? 0);
  161.             }
  162.         }
  163.         // Incident totals and pending grouped (single query).
  164.         $incidentRows $em->createQueryBuilder()
  165.             ->select('i.process AS processStatus, COUNT(i.id) AS total')
  166.             ->from(Incident::class, 'i')
  167.             ->join('i.marchand''m')
  168.             ->andWhere('m.id = :marchand')
  169.             ->setParameter('marchand'$entrepriseId)
  170.             ->groupBy('i.process')
  171.             ->getQuery()
  172.             ->getArrayResult();
  173.         $nbIncidents 0;
  174.         $pendingIncidents 0;
  175.         foreach ($incidentRows as $row) {
  176.             $count = (int) ($row['total'] ?? 0);
  177.             $nbIncidents += $count;
  178.             $status = (string) ($row['processStatus'] ?? '');
  179.             if ($status === '1' || $status === 'true') {
  180.                 $pendingIncidents $count;
  181.             }
  182.         }
  183.         // Monthly aggregates for current/previous years (single query for charts + percentages).
  184.         $monthlyRows $em->createQueryBuilder()
  185.             ->select('p.year AS yearValue, p.month AS monthValue')
  186.             ->addSelect('SUM(p.totalAmount) AS baseTotal')
  187.             ->addSelect('SUM(CASE WHEN p.paid = true THEN p.totalAmount ELSE 0 END) AS paidTotal')
  188.             ->addSelect('SUM(CASE WHEN p.paid = true THEN 1 ELSE 0 END) AS paidCount')
  189.             ->from(Payment::class, 'p')
  190.             ->join('p.marchand''m')
  191.             ->andWhere('m.id = :marchand')
  192.             ->andWhere('p.year IN (:years)')
  193.             ->setParameter('marchand'$entrepriseId)
  194.             ->setParameter('years', [(string) $previousYear, (string) $currentYear])
  195.             ->groupBy('p.year, p.month')
  196.             ->getQuery()
  197.             ->getArrayResult();
  198.         $monthlyIndex = [];
  199.         foreach ($monthlyRows as $row) {
  200.             $year = (int) ($row['yearValue'] ?? 0);
  201.             $month = (int) ($row['monthValue'] ?? 0);
  202.             if ($year <= || $month || $month 12) {
  203.                 continue;
  204.             }
  205.             $monthlyIndex[$year][$month] = [
  206.                 'baseTotal' => (float) ($row['baseTotal'] ?? 0),
  207.                 'paidTotal' => (float) ($row['paidTotal'] ?? 0),
  208.                 'paidCount' => (int) ($row['paidCount'] ?? 0),
  209.             ];
  210.         }
  211.         $ptageLast = [];
  212.         $ptageCurrent = [];
  213.         foreach (range(112) as $month) {
  214.             $ptageLast[] = (float) ($monthlyIndex[$previousYear][$month]['paidTotal'] ?? 0.0);
  215.             $ptageCurrent[] = (float) ($monthlyIndex[$currentYear][$month]['paidTotal'] ?? 0.0);
  216.         }
  217.         $countLastTwo = (int) ($monthlyIndex[$yearLastTwo][$lastTwoMonth]['paidCount'] ?? 0);
  218.         $countLastOne = (int) ($monthlyIndex[$yearLastOne][$lastOneMonth]['paidCount'] ?? 0);
  219.         $pourcentageLastTwoMonth $totalLoyers round(($countLastTwo $totalLoyers) * 1001) : 0.0;
  220.         $pourcentageLastOneMonth $totalLoyers round(($countLastOne $totalLoyers) * 1001) : 0.0;
  221.         $baseLastTwo = (float) ($monthlyIndex[$yearLastTwo][$lastTwoMonth]['baseTotal'] ?? 0.0);
  222.         $paidLastTwo = (float) ($monthlyIndex[$yearLastTwo][$lastTwoMonth]['paidTotal'] ?? 0.0);
  223.         $PaymentAmountLastTwoMonth $baseLastTwo round(($paidLastTwo $baseLastTwo) * 1001) : 0.0;
  224.         $baseLastOne = (float) ($monthlyIndex[$yearLastOne][$lastOneMonth]['baseTotal'] ?? 0.0);
  225.         $paidLastOne = (float) ($monthlyIndex[$yearLastOne][$lastOneMonth]['paidTotal'] ?? 0.0);
  226.         $PaymentAmountLastOneMonth $baseLastOne round(($paidLastOne $baseLastOne) * 1001) : 0.0;
  227.         // Recent unpaid payments / logs.
  228.         $marchand = (int) $request->query->get('marchand', (string) $entrepriseId);
  229.         if ($marchand !== $entrepriseId) {
  230.             $marchand $entrepriseId;
  231.         }
  232.         $recentUnpaid $paymentRepo->findBy(
  233.             ['marchand' => $marchand'paid' => false],
  234.             ['id' => 'DESC'],
  235.             4
  236.         );
  237.         $logs $em->getRepository(Log::class)->findBy(
  238.             ['marchand' => $marchand],
  239.             ['id' => 'DESC'],
  240.             5
  241.         );
  242.         $subscriptionRenewalData $this->resolveSubscriptionRenewalCardData($user$subscriptionRepository);
  243.         return $this->render('dashboard/long_term.html.twig'array_merge([
  244.             'habitatEnBail'             => $habitatEnBail,
  245.             'habitatLibre'              => $habitatLibre,
  246.             'invoiceUnpaid'             => $invoiceUnpaidCount,
  247.             'invoicePaid'               => $invoicePaidCount,
  248.             'paymentList'               => $recentUnpaid,
  249.             'ptageCurrent'              => json_encode($ptageCurrentJSON_THROW_ON_ERROR),
  250.             'ptageLast'                 => json_encode($ptageLastJSON_THROW_ON_ERROR),
  251.             'lastYear'                  => $previousYear,
  252.             'currentYear'               => $currentYear,
  253.             'pourcentage'               => $pourcentageLastOneMonth,
  254.             'pourcentageLastMonth'      => $pourcentageLastTwoMonth,
  255.             'lastTwoMonth'              => $this->processMonth((string) $lastTwoMonth),
  256.             'lastOneMonth'              => $this->processMonth((string) $lastOneMonth),
  257.             'PaymentAmountLastTwoMonth' => $PaymentAmountLastTwoMonth,
  258.             'PaymentAmountLastOneMonth' => $PaymentAmountLastOneMonth,
  259.             'NbreIncident'              => $nbIncidents,
  260.             'PendingIncide'             => $pendingIncidents,
  261.             'logs' => $logs,
  262.         ], $subscriptionRenewalData));
  263.     }
  264.     /**
  265.      * Displays the regular Short Term overview based on a user short term mode.
  266.      */
  267.     private function _handleShortTermRequest(
  268.         ManagerRegistry $doctrine,
  269.         RequestStack $requestStack,
  270.         Request $request,
  271.         SubscriptionRepository $subscriptionRepository
  272.     ): Response {
  273.         $em $doctrine->getManager();
  274.         $user $this->getUser();
  275.         if (!$user instanceof User) {
  276.             return $this->redirectToRoute('app_logout');
  277.         }
  278.         $entreprise $user->getEntreprise();
  279.         if (!$entreprise || !$entreprise->getId()) {
  280.             return $this->redirectToRoute('app_logout');
  281.         }
  282.         $habitats $em->getRepository(Habitat::class);
  283.         $incidentRepo $em->getRepository(Incident::class);
  284.         // Read optional filters (dates, status, term, property, marchand…)
  285.         $filters = [
  286.             'from'          => $request->query->get('from') ? new \DateTimeImmutable($request->query->get('from')) : null,
  287.             'to'            => $request->query->get('to')   ? new \DateTimeImmutable($request->query->get('to'))   : null,
  288.             'status'        => $request->query->get('status''all'),
  289.             'term'          => $request->query->get('term''all'),
  290.             'propertyId'    => $request->query->getInt('propertyId') ?: null,
  291.             'marchandId'    => $request->query->getInt('marchandId') ?: null,
  292.             'currencySymbol'=> '$'// or derive from your tenant/company settings
  293.         ];
  294.         $reservations $em->getRepository(Reservation::class);
  295.         $totalUnits $habitats->countActive($entreprise->getId()); // for occupancy rate
  296.         $data $reservations->dashboardData($filters$totalUnits$entreprise->getId());
  297.         $pendingIncidents  \count($incidentRepo->findBy(['marchand' => $entreprise'process' => '1']));
  298.         $subscriptionRenewalData $this->resolveSubscriptionRenewalCardData($user$subscriptionRepository);
  299.         return $this->render('dashboard/short_term.html.twig'array_merge([
  300.             'stats'  => $data['stats'],
  301.             'series' => $data['series'],
  302.             'table'  => $data['table'],
  303.             'lists'  => $data['lists'],
  304.             'filters'=> $filters,
  305.             'currentYear' => date('Y'),
  306.             'currentMonth' =>  $this->processMonth(date('n')),
  307.             'PendingIncide' => $pendingIncidents
  308.         ], $subscriptionRenewalData));
  309.     }
  310.     private function resolveSubscriptionRenewalCardData(
  311.         User $user,
  312.         SubscriptionRepository $subscriptionRepository
  313.     ): array {
  314.         $default = [
  315.             'showSubscriptionRenewalCard' => false,
  316.             'subscriptionPackageName' => null,
  317.             'subscriptionStartAt' => null,
  318.             'subscriptionEndAt' => null,
  319.         ];
  320.         $entreprise $user->getEntreprise();
  321.         if (!$entreprise) {
  322.             return $default;
  323.         }
  324.         $subscriptions $subscriptionRepository->findBy(
  325.             ['entreprise' => $entreprise],
  326.             ['endAt' => 'DESC''id' => 'DESC']
  327.         );
  328.         if (!$subscriptions) {
  329.             return $default;
  330.         }
  331.         $now = new \DateTimeImmutable();
  332.         $subscription null;
  333.         foreach ($subscriptions as $candidate) {
  334.             $candidateStartAt $candidate->getStartAt();
  335.             $candidateEndAt $candidate->getEndAt();
  336.             if (
  337.                 $candidateStartAt instanceof \DateTimeInterface
  338.                 && $candidateEndAt instanceof \DateTimeInterface
  339.                 && $candidateStartAt <= $now
  340.                 && $candidateEndAt >= $now
  341.             ) {
  342.                 $subscription $candidate;
  343.                 break;
  344.             }
  345.         }
  346.         if ($subscription === null) {
  347.             $subscription $subscriptions[0];
  348.         }
  349.         $packageName trim((string) ($subscription->getPackage()?->getName() ?? ''));
  350.         if ($packageName !== '' && strcasecmp($packageName'Essai') === 0) {
  351.             return $default;
  352.         }
  353.         $startAt $subscription->getStartAt();
  354.         $endAt $subscription->getEndAt();
  355.         $isValidByDates =
  356.             $startAt instanceof \DateTimeInterface
  357.             && $endAt instanceof \DateTimeInterface
  358.             && $startAt <= $now
  359.             && $endAt >= $now;
  360.         $isValid $isValidByDates && $subscription->isStatus() === true;
  361.         if ($isValid) {
  362.             return $default;
  363.         }
  364.         return [
  365.             'showSubscriptionRenewalCard' => true,
  366.             'subscriptionPackageName' => $packageName !== '' $packageName null,
  367.             'subscriptionStartAt' => $startAt,
  368.             'subscriptionEndAt' => $endAt,
  369.         ];
  370.     }
  371.     /**
  372.      * French month label from numeric month.
  373.      */
  374.     public function processMonth(string $month): string
  375.     {
  376.         return match ($month) {
  377.             '2'  => 'Février',
  378.             '3'  => 'Mars',
  379.             '4'  => 'Avril',
  380.             '5'  => 'Mai',
  381.             '6'  => 'Juin',
  382.             '7'  => 'Juillet',
  383.             '8'  => 'Août',
  384.             '9'  => 'Septembre',
  385.             '10' => 'Octobre',
  386.             '11' => 'Novembre',
  387.             '12' => 'Décembre',
  388.             default => 'Janvier',
  389.         };
  390.     }
  391.     private function syncExpiredSubscriptions(
  392.         SubscriptionRepository $subscriptionRepository,
  393.         EntityManagerInterface $entityManager
  394.     ): void
  395.     {
  396.         $now = new \DateTimeImmutable();
  397.         $expiredSubscriptions $subscriptionRepository->findExpiredActiveSubscriptions($now);
  398.         if (!$expiredSubscriptions) {
  399.             return;
  400.         }
  401.         $entreprisesToCheck = [];
  402.         foreach ($expiredSubscriptions as $subscription) {
  403.             $subscription->setStatus(false);
  404.             $entreprise $subscription->getEntreprise();
  405.             if ($entreprise) {
  406.                 $key $entreprise->getId() ?? spl_object_id($entreprise);
  407.                 $entreprisesToCheck[$key] = $entreprise;
  408.             }
  409.         }
  410.         foreach ($entreprisesToCheck as $entreprise) {
  411.             if (!$subscriptionRepository->hasActiveSubscriptionForEntreprise($entreprise$now)) {
  412.                 $entreprise->setStatus(false);
  413.             }
  414.         }
  415.         $entityManager->flush();
  416.     }
  417. }