src/Controller/DashboardController.php line 25
<?phpnamespace App\Controller;use App\Entity\Habitat;use App\Entity\Incident;use App\Entity\Location;use App\Entity\Log;use App\Entity\Payment;use App\Entity\Reservation;use App\Entity\User;use App\Repository\SubscriptionRepository;use Doctrine\ORM\EntityManagerInterface;use Doctrine\Persistence\ManagerRegistry;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\RequestStack;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Annotation\Route;#[Route('/admin/dashboard')]class DashboardController extends AbstractController{#[Route('/', name: 'app_dashboard')]public function index(RequestStack $requestStack, SubscriptionRepository $subscriptionRepository, EntityManagerInterface $entityManager): Response {$user = $this->getUser();if (!$user instanceof User) {return $this->redirectToRoute('app_logout');}$this->syncExpiredSubscriptions($subscriptionRepository, $entityManager);$show = $requestStack->getSession()->get('dashboard-overview', 'long_term');$subscriptionRenewalData = $this->resolveSubscriptionRenewalCardData($user, $subscriptionRepository);return $this->render('dashboard/index.html.twig', array_merge(['user' => $user,'show' => $show], $subscriptionRenewalData));}#[Route('/view/{show}', name: 'start.dashboard.toggle.view', methods: ['GET'])]public function indexActionToggle(RequestStack $requestStack, string $show, ManagerRegistry $doctrine): Response{$em = $doctrine->getManager();$authUser = $this->getUser();if (!$authUser) {return $this->redirectToRoute('app_logout');}if ('long_term' === $show) {$requestStack->getSession()->set('dashboard-overview', 'long_term');$authUser->setMode('long_term');} else {$requestStack->getSession()->set('dashboard-overview', 'short_term');$authUser->setMode('short_term');}$em->flush();return $this->forward('App\Controller\DashboardController::index');}/*** Gets the reservation overview.* @throws \JsonException*/#[Route('/mode', name: 'dashboard.get.mode', methods: ['GET'])]public function getModeAction(ManagerRegistry $doctrine,RequestStack $requestStack,Request $request,SubscriptionRepository $subscriptionRepository): Response{$mode = $request->query->get('mode', 'long_term');if ('long_term' === $mode) {return $this->_handleLongTermRequest($doctrine, $requestStack, $request, $subscriptionRepository);} else {return $this->_handleShortTermRequest($doctrine, $requestStack, $request, $subscriptionRepository);}}/*** Displays the regular Long Term overview based on a user long term mode.*/private function _handleLongTermRequest(ManagerRegistry $doctrine,RequestStack $requestStack,Request $request,SubscriptionRepository $subscriptionRepository): Response {$em = $doctrine->getManager();$user = $this->getUser();if (!$user instanceof User) {return $this->redirectToRoute('app_logout');}$entreprise = $user->getEntreprise();if (!$entreprise || !$entreprise->getId()) {return $this->redirectToRoute('app_logout');}if (!$user->getMode()) {$user->setMode('light');$em->flush();}$requestStack->getSession()->remove('location-details-overview');$now = new \DateTimeImmutable();$currentYear = (int) $now->format('Y');$previousYear = $currentYear - 1;$lastTwoMonthDate = $now->modify('-2 months');$lastOneMonthDate = $now->modify('-1 month');$lastTwoMonth = (int) $lastTwoMonthDate->format('n');$lastOneMonth = (int) $lastOneMonthDate->format('n');$yearLastTwo = (int) $lastTwoMonthDate->format('Y');$yearLastOne = (int) $lastOneMonthDate->format('Y');$entrepriseId = (int) $entreprise->getId();$paymentRepo = $em->getRepository(Payment::class);// Invoice counts grouped by paid status (single query).$invoiceRows = $em->createQueryBuilder()->select('p.paid AS paidStatus, COUNT(p.id) AS total')->from(Payment::class, 'p')->join('p.marchand', 'm')->andWhere('m.id = :marchand')->setParameter('marchand', $entrepriseId)->groupBy('p.paid')->getQuery()->getArrayResult();$invoicePaidCount = 0;$invoiceUnpaidCount = 0;foreach ($invoiceRows as $row) {$isPaid = filter_var((string) ($row['paidStatus'] ?? ''), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);if ($isPaid === null) {$isPaid = ((int) ($row['paidStatus'] ?? 0)) === 1;}if ($isPaid) {$invoicePaidCount = (int) ($row['total'] ?? 0);} else {$invoiceUnpaidCount = (int) ($row['total'] ?? 0);}}// Active leases count.$totalLoyers = (int) $em->createQueryBuilder()->select('COUNT(l.id)')->from(Location::class, 'l')->join('l.marchand', 'm')->andWhere('m.id = :marchand')->andWhere('l.occuper = :occupied')->setParameter('marchand', $entrepriseId)->setParameter('occupied', true)->getQuery()->getSingleScalarResult();// Habitat occupation counts grouped (single query).$habitatRows = $em->createQueryBuilder()->select('h.occuper AS occupiedStatus, COUNT(h.id) AS total')->from(Habitat::class, 'h')->join('h.marchand', 'm')->andWhere('m.id = :marchand')->setParameter('marchand', $entrepriseId)->groupBy('h.occuper')->getQuery()->getArrayResult();$habitatEnBail = 0;$habitatLibre = 0;foreach ($habitatRows as $row) {$isOccupied = filter_var((string) ($row['occupiedStatus'] ?? ''), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);if ($isOccupied === null) {$isOccupied = ((int) ($row['occupiedStatus'] ?? 0)) === 1;}if ($isOccupied) {$habitatEnBail = (int) ($row['total'] ?? 0);} else {$habitatLibre = (int) ($row['total'] ?? 0);}}// Incident totals and pending grouped (single query).$incidentRows = $em->createQueryBuilder()->select('i.process AS processStatus, COUNT(i.id) AS total')->from(Incident::class, 'i')->join('i.marchand', 'm')->andWhere('m.id = :marchand')->setParameter('marchand', $entrepriseId)->groupBy('i.process')->getQuery()->getArrayResult();$nbIncidents = 0;$pendingIncidents = 0;foreach ($incidentRows as $row) {$count = (int) ($row['total'] ?? 0);$nbIncidents += $count;$status = (string) ($row['processStatus'] ?? '');if ($status === '1' || $status === 'true') {$pendingIncidents = $count;}}// Monthly aggregates for current/previous years (single query for charts + percentages).$monthlyRows = $em->createQueryBuilder()->select('p.year AS yearValue, p.month AS monthValue')->addSelect('SUM(p.totalAmount) AS baseTotal')->addSelect('SUM(CASE WHEN p.paid = true THEN p.totalAmount ELSE 0 END) AS paidTotal')->addSelect('SUM(CASE WHEN p.paid = true THEN 1 ELSE 0 END) AS paidCount')->from(Payment::class, 'p')->join('p.marchand', 'm')->andWhere('m.id = :marchand')->andWhere('p.year IN (:years)')->setParameter('marchand', $entrepriseId)->setParameter('years', [(string) $previousYear, (string) $currentYear])->groupBy('p.year, p.month')->getQuery()->getArrayResult();$monthlyIndex = [];foreach ($monthlyRows as $row) {$year = (int) ($row['yearValue'] ?? 0);$month = (int) ($row['monthValue'] ?? 0);if ($year <= 0 || $month < 1 || $month > 12) {continue;}$monthlyIndex[$year][$month] = ['baseTotal' => (float) ($row['baseTotal'] ?? 0),'paidTotal' => (float) ($row['paidTotal'] ?? 0),'paidCount' => (int) ($row['paidCount'] ?? 0),];}$ptageLast = [];$ptageCurrent = [];foreach (range(1, 12) as $month) {$ptageLast[] = (float) ($monthlyIndex[$previousYear][$month]['paidTotal'] ?? 0.0);$ptageCurrent[] = (float) ($monthlyIndex[$currentYear][$month]['paidTotal'] ?? 0.0);}$countLastTwo = (int) ($monthlyIndex[$yearLastTwo][$lastTwoMonth]['paidCount'] ?? 0);$countLastOne = (int) ($monthlyIndex[$yearLastOne][$lastOneMonth]['paidCount'] ?? 0);$pourcentageLastTwoMonth = $totalLoyers > 0 ? round(($countLastTwo / $totalLoyers) * 100, 1) : 0.0;$pourcentageLastOneMonth = $totalLoyers > 0 ? round(($countLastOne / $totalLoyers) * 100, 1) : 0.0;$baseLastTwo = (float) ($monthlyIndex[$yearLastTwo][$lastTwoMonth]['baseTotal'] ?? 0.0);$paidLastTwo = (float) ($monthlyIndex[$yearLastTwo][$lastTwoMonth]['paidTotal'] ?? 0.0);$PaymentAmountLastTwoMonth = $baseLastTwo > 0 ? round(($paidLastTwo / $baseLastTwo) * 100, 1) : 0.0;$baseLastOne = (float) ($monthlyIndex[$yearLastOne][$lastOneMonth]['baseTotal'] ?? 0.0);$paidLastOne = (float) ($monthlyIndex[$yearLastOne][$lastOneMonth]['paidTotal'] ?? 0.0);$PaymentAmountLastOneMonth = $baseLastOne > 0 ? round(($paidLastOne / $baseLastOne) * 100, 1) : 0.0;// Recent unpaid payments / logs.$marchand = (int) $request->query->get('marchand', (string) $entrepriseId);if ($marchand !== $entrepriseId) {$marchand = $entrepriseId;}$recentUnpaid = $paymentRepo->findBy(['marchand' => $marchand, 'paid' => false],['id' => 'DESC'],4);$logs = $em->getRepository(Log::class)->findBy(['marchand' => $marchand],['id' => 'DESC'],5);$subscriptionRenewalData = $this->resolveSubscriptionRenewalCardData($user, $subscriptionRepository);return $this->render('dashboard/long_term.html.twig', array_merge(['habitatEnBail' => $habitatEnBail,'habitatLibre' => $habitatLibre,'invoiceUnpaid' => $invoiceUnpaidCount,'invoicePaid' => $invoicePaidCount,'paymentList' => $recentUnpaid,'ptageCurrent' => json_encode($ptageCurrent, JSON_THROW_ON_ERROR),'ptageLast' => json_encode($ptageLast, JSON_THROW_ON_ERROR),'lastYear' => $previousYear,'currentYear' => $currentYear,'pourcentage' => $pourcentageLastOneMonth,'pourcentageLastMonth' => $pourcentageLastTwoMonth,'lastTwoMonth' => $this->processMonth((string) $lastTwoMonth),'lastOneMonth' => $this->processMonth((string) $lastOneMonth),'PaymentAmountLastTwoMonth' => $PaymentAmountLastTwoMonth,'PaymentAmountLastOneMonth' => $PaymentAmountLastOneMonth,'NbreIncident' => $nbIncidents,'PendingIncide' => $pendingIncidents,'logs' => $logs,], $subscriptionRenewalData));}/*** Displays the regular Short Term overview based on a user short term mode.*/private function _handleShortTermRequest(ManagerRegistry $doctrine,RequestStack $requestStack,Request $request,SubscriptionRepository $subscriptionRepository): Response {$em = $doctrine->getManager();$user = $this->getUser();if (!$user instanceof User) {return $this->redirectToRoute('app_logout');}$entreprise = $user->getEntreprise();if (!$entreprise || !$entreprise->getId()) {return $this->redirectToRoute('app_logout');}$habitats = $em->getRepository(Habitat::class);$incidentRepo = $em->getRepository(Incident::class);// Read optional filters (dates, status, term, property, marchand…)$filters = ['from' => $request->query->get('from') ? new \DateTimeImmutable($request->query->get('from')) : null,'to' => $request->query->get('to') ? new \DateTimeImmutable($request->query->get('to')) : null,'status' => $request->query->get('status', 'all'),'term' => $request->query->get('term', 'all'),'propertyId' => $request->query->getInt('propertyId') ?: null,'marchandId' => $request->query->getInt('marchandId') ?: null,'currencySymbol'=> '$', // or derive from your tenant/company settings];$reservations = $em->getRepository(Reservation::class);$totalUnits = $habitats->countActive($entreprise->getId()); // for occupancy rate$data = $reservations->dashboardData($filters, $totalUnits, $entreprise->getId());$pendingIncidents = \count($incidentRepo->findBy(['marchand' => $entreprise, 'process' => '1']));$subscriptionRenewalData = $this->resolveSubscriptionRenewalCardData($user, $subscriptionRepository);return $this->render('dashboard/short_term.html.twig', array_merge(['stats' => $data['stats'],'series' => $data['series'],'table' => $data['table'],'lists' => $data['lists'],'filters'=> $filters,'currentYear' => date('Y'),'currentMonth' => $this->processMonth(date('n')),'PendingIncide' => $pendingIncidents], $subscriptionRenewalData));}private function resolveSubscriptionRenewalCardData(User $user,SubscriptionRepository $subscriptionRepository): array {$default = ['showSubscriptionRenewalCard' => false,'subscriptionPackageName' => null,'subscriptionStartAt' => null,'subscriptionEndAt' => null,];$entreprise = $user->getEntreprise();if (!$entreprise) {return $default;}$subscriptions = $subscriptionRepository->findBy(['entreprise' => $entreprise],['endAt' => 'DESC', 'id' => 'DESC']);if (!$subscriptions) {return $default;}$now = new \DateTimeImmutable();$subscription = null;foreach ($subscriptions as $candidate) {$candidateStartAt = $candidate->getStartAt();$candidateEndAt = $candidate->getEndAt();if ($candidateStartAt instanceof \DateTimeInterface&& $candidateEndAt instanceof \DateTimeInterface&& $candidateStartAt <= $now&& $candidateEndAt >= $now) {$subscription = $candidate;break;}}if ($subscription === null) {$subscription = $subscriptions[0];}$packageName = trim((string) ($subscription->getPackage()?->getName() ?? ''));if ($packageName !== '' && strcasecmp($packageName, 'Essai') === 0) {return $default;}$startAt = $subscription->getStartAt();$endAt = $subscription->getEndAt();$isValidByDates =$startAt instanceof \DateTimeInterface&& $endAt instanceof \DateTimeInterface&& $startAt <= $now&& $endAt >= $now;$isValid = $isValidByDates && $subscription->isStatus() === true;if ($isValid) {return $default;}return ['showSubscriptionRenewalCard' => true,'subscriptionPackageName' => $packageName !== '' ? $packageName : null,'subscriptionStartAt' => $startAt,'subscriptionEndAt' => $endAt,];}/*** French month label from numeric month.*/public function processMonth(string $month): string{return match ($month) {'2' => 'Février','3' => 'Mars','4' => 'Avril','5' => 'Mai','6' => 'Juin','7' => 'Juillet','8' => 'Août','9' => 'Septembre','10' => 'Octobre','11' => 'Novembre','12' => 'Décembre',default => 'Janvier',};}private function syncExpiredSubscriptions(SubscriptionRepository $subscriptionRepository,EntityManagerInterface $entityManager): void{$now = new \DateTimeImmutable();$expiredSubscriptions = $subscriptionRepository->findExpiredActiveSubscriptions($now);if (!$expiredSubscriptions) {return;}$entreprisesToCheck = [];foreach ($expiredSubscriptions as $subscription) {$subscription->setStatus(false);$entreprise = $subscription->getEntreprise();if ($entreprise) {$key = $entreprise->getId() ?? spl_object_id($entreprise);$entreprisesToCheck[$key] = $entreprise;}}foreach ($entreprisesToCheck as $entreprise) {if (!$subscriptionRepository->hasActiveSubscriptionForEntreprise($entreprise, $now)) {$entreprise->setStatus(false);}}$entityManager->flush();}}