src/Controller/Admin/DashboardController.php line 97

  1. <?php
  2. namespace App\Controller\Admin;
  3. use App\Controller\Admin\PaidOrdersCrudController;
  4. use App\Controller\Admin\UnpaidOrdersCrudController;
  5. use App\Entity\Application;
  6. use App\Entity\CurrencyExchangeRate;
  7. use App\Entity\EntityLog;
  8. use App\Entity\ExportExcel;
  9. use App\Entity\FrontTheme;
  10. use App\Entity\Link;
  11. use App\Entity\LinkType;
  12. use App\Entity\LogHistory;
  13. use App\Entity\Notification;
  14. use App\Entity\Role;
  15. use App\Entity\Settings;
  16. use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
  17. use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
  18. use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\Routing\Annotation\Route;
  21. use App\Entity\User;
  22. use App\Entity\WebsiteTheme;
  23. use App\Form\Admin\BroadcastPushNotificationType;
  24. use App\Form\Admin\GroupPushNotificationType;
  25. use App\Form\Admin\TargetedPushNotificationType;
  26. use App\IlaveU\ShopBundle\Entity\Customer\Customer;
  27. use App\IlaveU\ShopBundle\Entity\Customer\CustomerGroup;
  28. use App\IlaveU\ShopBundle\Repository\Customer\CustomerGroupRepository;
  29. use App\IlaveU\ShopBundle\Repository\Customer\CustomerRepository;
  30. use App\Repository\UserRepository;
  31. use App\Service\ExpoPushNotificationService;
  32. use App\IlaveU\ShopBundle\Entity\Order\Order;
  33. use App\IlaveU\ShopBundle\Entity\Product\Product;
  34. use App\IlaveU\ShopBundle\Entity\Store\Store;
  35. use App\IlaveU\ShopBundle\Entity\Vendor\Vendor;
  36. use App\IlaveU\ShopBundle\Service\IlaveUShopStatisticProvider;
  37. use App\Repository\ApplicationRepository;
  38. use App\Service\IlaveUSettingsProvider;
  39. use Doctrine\Persistence\ManagerRegistry;
  40. use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
  41. use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
  42. use Symfony\Component\Filesystem\Filesystem;
  43. use Symfony\Component\Finder\Finder;
  44. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  45. use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
  46. use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
  47. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityRepositoryInterface;
  48. use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
  49. use Symfony\Component\HttpFoundation\JsonResponse;
  50. use Symfony\Component\HttpFoundation\Request;
  51. use Symfony\Component\HttpFoundation\RequestStack;
  52. use Symfony\Component\HttpFoundation\StreamedResponse;
  53. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  54. use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
  55. class DashboardController extends AbstractDashboardController
  56. {
  57.     public function __construct(
  58.         private readonly ManagerRegistry $doctrine,
  59.         private readonly IlaveUShopStatisticProvider $ilaveShopStatisticProvider,
  60.         private readonly RequestStack $requestStack,
  61.     ) {}
  62.     #[Route(path'/admin'name'admin_non_locale')]
  63.     public function indexNonLocale(Request $request): Response
  64.     {
  65.         return $this->redirectToRoute("admin", ["_locale" => $request->getLocale()]);
  66.     }
  67.     #[Route('/{_locale}/admin/website-theme/grapesjs_edit'name'website_theme_grapesjs_edit')]
  68.     public function website_theme_grapesjs_edit(): Response
  69.     {
  70.         return $this->render('@IlaveU/FrontBundle/Themes/' $this->container->get('twig')->getGlobals()["settings"]->get()->getAssetFolderName() . '/templates/admin/website-theme/grapesjs.html.twig');
  71.     }
  72.     #[Route(path'/{_locale}/admin/apps-store'name'app_store')]
  73.     public function appStore(Request $requestApplicationRepository $applicationRepositoryIlaveUSettingsProvider $ilaveSettingsProvider): Response
  74.     {
  75.         $appsArray = [];
  76.         foreach ($applicationRepository->findAll() as $singleApp) {
  77.             $appsArray[] = [
  78.                 "id" => $singleApp->getId(),
  79.                 "name" => $singleApp->getName(),
  80.                 "image" => $singleApp->getImage(),
  81.                 "price" => $singleApp->getPrice(),
  82.                 "pageUrl" => $singleApp->getPageUrl(),
  83.             ];
  84.         }
  85.         return $this->render("admin/app-store.html.twig", [
  86.             "installedApps" => $appsArray,
  87.             "JWT" => $ilaveSettingsProvider->createJWTForUser($this->getUser())
  88.         ]);
  89.     }
  90.     #[Route(path'/{_locale}/admin/push-notifications/broadcast'name'admin_push_broadcast')]
  91.     #[IsGranted('ROLE_ADMIN_DEV')]
  92.     public function pushBroadcast(
  93.         Request $request,
  94.         UserRepository $userRepository,
  95.         ExpoPushNotificationService $expoPush,
  96.     ): Response {
  97.         $recipientCount $userRepository->countUsersWithExponentPushToken();
  98.         $tokens $userRepository->findDistinctExponentPushTokens();
  99.         $form $this->createForm(BroadcastPushNotificationType::class);
  100.         $form->handleRequest($request);
  101.         if ($form->isSubmitted() && $form->isValid()) {
  102.             $data $form->getData();
  103.             $title = (string) ($data['title'] ?? '');
  104.             $message = (string) ($data['message'] ?? '');
  105.             $result $expoPush->sendToTokens($tokens$title$message);
  106.             if ($result['sent'] > 0) {
  107.                 $this->addFlash('success'sprintf(
  108.                     'Notifications envoyées : %d succès.',
  109.                     $result['sent']
  110.                 ));
  111.             }
  112.             if ($result['failed'] > 0) {
  113.                 $this->addFlash('warning'sprintf(
  114.                     'Échecs partiels : %d envoi(s) en erreur.',
  115.                     $result['failed']
  116.                 ));
  117.             }
  118.             foreach (array_slice($result['errors'], 05) as $err) {
  119.                 $this->addFlash('danger'$err);
  120.             }
  121.             if ($result['sent'] === && $result['failed'] === && $tokens === []) {
  122.                 $this->addFlash('warning''Aucun utilisateur avec un token Expo enregistré.');
  123.             }
  124.             return $this->redirectToRoute('admin_push_broadcast', ['_locale' => $request->getLocale()]);
  125.         }
  126.         return $this->render('admin/push-broadcast.html.twig', [
  127.             'form' => $form->createView(),
  128.             'recipientCount' => $recipientCount,
  129.         ]);
  130.     }
  131.     #[Route(path'/{_locale}/admin/push-notifications/search/customers'name'admin_push_search_customers')]
  132.     #[IsGranted('ROLE_ADMIN_DEV')]
  133.     public function pushSearchCustomers(Request $requestCustomerRepository $customerRepository): JsonResponse
  134.     {
  135.         $query = (string) $request->query->get('query''');
  136.         $results = [];
  137.         foreach ($customerRepository->searchForPushNotification($query) as $customer) {
  138.             $results[] = [
  139.                 'entityId' => (string) $customer->getId(),
  140.                 'entityAsString' => TargetedPushNotificationType::formatCustomerLabel($customer),
  141.             ];
  142.         }
  143.         return $this->json(['results' => $results'next_page' => null]);
  144.     }
  145.     #[Route(path'/{_locale}/admin/push-notifications/search/users'name'admin_push_search_users')]
  146.     #[IsGranted('ROLE_ADMIN_DEV')]
  147.     public function pushSearchUsers(Request $requestUserRepository $userRepository): JsonResponse
  148.     {
  149.         $query = (string) $request->query->get('query''');
  150.         $results = [];
  151.         foreach ($userRepository->searchForPushNotification($query) as $user) {
  152.             $results[] = [
  153.                 'entityId' => (string) $user->getId(),
  154.                 'entityAsString' => TargetedPushNotificationType::formatUserLabel($user),
  155.             ];
  156.         }
  157.         return $this->json(['results' => $results'next_page' => null]);
  158.     }
  159.      #[Route(path'/{_locale}/admin/push-notifications/targeted'name'admin_push_targeted')]
  160.     #[IsGranted('ROLE_ADMIN_DEV')]
  161.     public function pushTargeted(
  162.         Request $request,
  163.         CustomerRepository $customerRepository,
  164.         ExpoPushNotificationService $expoPush,
  165.     ): Response {
  166.         $form $this->createForm(TargetedPushNotificationType::class);
  167.         $form->handleRequest($request);
  168.         if ($form->isSubmitted() && $form->isValid()) {
  169.             $data $form->getData();
  170.             $title = (string) ($data['title'] ?? '');
  171.             $message = (string) ($data['message'] ?? '');
  172.             $recipientType = (string) ($data['recipientType'] ?? 'customer');
  173.             $token null;
  174.             $recipientLabel '';
  175.             if ($recipientType === 'user') {
  176.                 $user $data['user'] ?? null;
  177.                 $token $user?->getExponentPushToken();
  178.                 $recipientLabel $user sprintf('compte %s'$user->getUsername()) : '';
  179.             } else {
  180.                 /** @var Customer|null $customer */
  181.                 $customer $data['customer'] ?? null;
  182.                 $token $customer?->getUser()?->getExponentPushToken();
  183.                 $recipientLabel $customer
  184.                     sprintf('client #%d — %s'$customer->getId(), $customer->getFullName())
  185.                     : '';
  186.             }
  187.             if ($token === null || trim((string) $token) === '') {
  188.                 $this->addFlash('danger'sprintf(
  189.                     'Aucun token Expo enregistré pour ce destinataire (%s). L’utilisateur doit activer les notifications sur l’app mobile.',
  190.                     $recipientLabel
  191.                 ));
  192.             } else {
  193.                 $result $expoPush->sendToTokens([$token], $title$message);
  194.                 if ($result['sent'] > 0) {
  195.                     $this->addFlash('success'sprintf(
  196.                         'Notification envoyée à %s.',
  197.                         $recipientLabel
  198.                     ));
  199.                 }
  200.                 if ($result['failed'] > 0) {
  201.                     $this->addFlash('warning''L’envoi a échoué côté Expo.');
  202.                 }
  203.                 foreach (array_slice($result['errors'], 05) as $err) {
  204.                     $this->addFlash('danger'$err);
  205.                 }
  206.             }
  207.             return $this->redirectToRoute('admin_push_targeted', ['_locale' => $request->getLocale()]);
  208.         }
  209.         return $this->render('admin/push-targeted.html.twig', [
  210.             'form' => $form->createView(),
  211.             'mobileTokenCustomerCount' => $customerRepository->countCustomersWithMobilePushToken(),
  212.         ]);
  213.     }
  214.     
  215.     
  216.     #[Route(path'/{_locale}/admin/push-notifications/targeted/export-mobile-tokens'name'admin_push_targeted_export')]
  217.     #[IsGranted('ROLE_ADMIN_DEV')]
  218.     public function pushTargetedExportMobileTokens(CustomerRepository $customerRepository): StreamedResponse
  219.     {
  220.         $customers $customerRepository->findCustomersWithMobilePushToken();
  221.         $spreadsheet = new Spreadsheet();
  222.         $sheet $spreadsheet->getActiveSheet();
  223.         $sheet->setTitle('Clients token mobile');
  224.         $sheet->fromArray([
  225.             'ID client',
  226.             'Prénom',
  227.             'Nom',
  228.             'Email',
  229.             'Téléphone',
  230.             'Groupe client',
  231.             'ID compte',
  232.             'Identifiant',
  233.             'Token Expo',
  234.         ], null'A1');
  235.         $row 2;
  236.         foreach ($customers as $customer) {
  237.             $user $customer->getUser();
  238.             $group $customer->getCustomerGroup();
  239.             $sheet->fromArray([
  240.                 $customer->getId(),
  241.                 $customer->getFirstName(),
  242.                 $customer->getLastName(),
  243.                 $customer->getEmail(),
  244.                 $customer->getPhone(),
  245.                 $group?->getName(),
  246.                 $user?->getId(),
  247.                 $user?->getUsername(),
  248.                 $user?->getExponentPushToken(),
  249.             ], null'A' $row);
  250.             ++$row;
  251.         }
  252.         foreach (range('A''I') as $column) {
  253.             $sheet->getColumnDimension($column)->setAutoSize(true);
  254.         }
  255.         $filename sprintf('clients-token-mobile-%s.xlsx', (new \DateTime())->format('Y-m-d-His'));
  256.         $response = new StreamedResponse(static function () use ($spreadsheet): void {
  257.             $writer = new XlsxWriter($spreadsheet);
  258.             $writer->save('php://output');
  259.         });
  260.         $response->headers->set('Content-Type''application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  261.         $response->headers->set('Content-Disposition'sprintf('attachment; filename="%s"'$filename));
  262.         return $response;
  263.     }
  264.     
  265.     #[Route(path'/{_locale}/admin/push-notifications/group'name'admin_push_group')]
  266.     #[IsGranted('ROLE_ADMIN_DEV')]
  267.     public function pushGroup(
  268.         Request $request,
  269.         CustomerGroupRepository $customerGroupRepository,
  270.         ExpoPushNotificationService $expoPush,
  271.     ): Response {
  272.         $form $this->createForm(GroupPushNotificationType::class);
  273.         $form->handleRequest($request);
  274.         $recipientCount null;
  275.         $memberCount null;
  276.         $selectedGroup null;
  277.         if ($form->isSubmitted() && $form->isValid()) {
  278.             $data $form->getData();
  279.             /** @var CustomerGroup $group */
  280.             $group $data['customerGroup'];
  281.             $title = (string) ($data['title'] ?? '');
  282.             $message = (string) ($data['message'] ?? '');
  283.             $tokens $customerGroupRepository->findDistinctExponentPushTokensForGroup($group);
  284.             $memberCount $customerGroupRepository->countCustomersInGroup($group);
  285.             $groupLabel sprintf('%s (#%d)'$group->getName(), $group->getId());
  286.             if ($tokens === []) {
  287.                 $this->addFlash('warning'sprintf(
  288.                     'Aucun client du groupe « %s » n’a de token Expo enregistré (%d client(s) dans le groupe).',
  289.                     $groupLabel,
  290.                     $memberCount
  291.                 ));
  292.             } else {
  293.                 $result $expoPush->sendToTokens($tokens$title$message);
  294.                 if ($result['sent'] > 0) {
  295.                     $this->addFlash('success'sprintf(
  296.                         'Notifications envoyées au groupe « %s » : %d succès (%d destinataire(s) avec token mobile sur %d client(s)).',
  297.                         $groupLabel,
  298.                         $result['sent'],
  299.                         count($tokens),
  300.                         $memberCount
  301.                     ));
  302.                 }
  303.                 if ($result['failed'] > 0) {
  304.                     $this->addFlash('warning'sprintf(
  305.                         'Échecs partiels pour le groupe « %s » : %d envoi(s) en erreur.',
  306.                         $groupLabel,
  307.                         $result['failed']
  308.                     ));
  309.                 }
  310.                 foreach (array_slice($result['errors'], 05) as $err) {
  311.                     $this->addFlash('danger'$err);
  312.                 }
  313.             }
  314.             return $this->redirectToRoute('admin_push_group', ['_locale' => $request->getLocale()]);
  315.         }
  316.         if ($form->isSubmitted() && !$form->isValid()) {
  317.             $selectedGroup $form->get('customerGroup')->getData();
  318.         }
  319.         if ($selectedGroup instanceof CustomerGroup) {
  320.             $memberCount $customerGroupRepository->countCustomersInGroup($selectedGroup);
  321.             $recipientCount count($customerGroupRepository->findDistinctExponentPushTokensForGroup($selectedGroup));
  322.         }
  323.         return $this->render('admin/push-group.html.twig', [
  324.             'form' => $form->createView(),
  325.             'memberCount' => $memberCount,
  326.             'recipientCount' => $recipientCount,
  327.             'selectedGroup' => $selectedGroup,
  328.         ]);
  329.     }
  330.     
  331.     #[Route(path'/{_locale}/admin'name'admin')]
  332.     public function index(): Response
  333.     {
  334.         $url $this->requestStack->getCurrentRequest()->server->get('REQUEST_URI');
  335.         
  336.         $parsedUrl parse_url($url);
  337.         parse_str($parsedUrl['query'] ?? ''$queryParams);
  338.         if (count($queryParams) == 0) {
  339.             $queryParams['store'] = null;
  340.             $queryParams['range'] = null;
  341.             $queryParams['from'] = null;
  342.             $queryParams['to'] = null;
  343.         }
  344.         $range   $queryParams['range'] ?? 'today';
  345.         $from    $queryParams['from'] ?? null;
  346.         $to      $queryParams['to'] ?? null;
  347.         $storeParam   = (int)$queryParams['store'] ?? null;
  348.         $store null;
  349.         
  350.         
  351.         // Check if user has ROLE_STORE and get their store
  352.         if ($this->isGranted('ROLE_STORE')) {
  353.             $currentUser $this->getUser();
  354.             if ($currentUser) {
  355.                 $storeEntity $this->doctrine->getRepository(Store::class)->findOneBy(["user" => $currentUser]);
  356.                 $store $storeEntity;
  357.             }
  358.         } elseif ($storeParam != 0) {
  359.             $storeEntity $this->doctrine->getRepository(Store::class)->findOneBy(["id" => $storeParam]);
  360.             $store $storeEntity;
  361.         }
  362.         
  363.         
  364.         //dd($request);
  365.         // Get list of stores
  366.         $stores $this->doctrine->getRepository(Store::class)->findAll();
  367.         // Handle predefined ranges
  368.         $now = new \DateTimeImmutable();
  369.         // If custom date range provided, it overrides predefined range
  370.         if ($from && $to) {
  371.             $start \DateTimeImmutable::createFromFormat('Y-m-d'$from)?->setTime(00);
  372.             $end   \DateTimeImmutable::createFromFormat('Y-m-d'$to)?->setTime(235959);
  373.         } elseif ($range === 'today') {
  374.             $start $now->modify('today')->setTime(00);
  375.             $end $now->modify('today')->setTime(235959);
  376.         } elseif ($range === 'yesterday') {
  377.             $start $now->modify('yesterday')->setTime(00);
  378.             $end $now->modify('yesterday')->setTime(235959);
  379.         } elseif ($range === 'week') {
  380.             $start $now->modify('this week')->setTime(00);
  381.             $end $now->modify('next week')->setTime(00)->modify('-1 second');
  382.         } elseif ($range === 'month') {
  383.             $start $now->modify('first day of this month')->setTime(00);
  384.             $end $now->modify('last day of this month')->setTime(235959);
  385.         } elseif ($range === 'year') {
  386.             $start $now->modify('first day of January')->setTime(00);
  387.             $end $now->modify('last day of December')->setTime(235959);
  388.         } else { // Month By Default
  389.             $start $now->modify('today')->setTime(00);
  390.             $end $now->modify('today')->setTime(235959);
  391.         }
  392.         // If custom date range provided (overrides predefined range)
  393.         
  394.         $validOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  395.             ->select('count(orderAlias.id)')
  396.             ->andWhere("orderAlias.status <> 'draft'")
  397.             ->andWhere("orderAlias.status <> 'cancelled'")
  398.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  399.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  400.             ->setParameter('start'$start)
  401.             ->setParameter('end'$end);
  402.             if($store){
  403.                 $validOrders->andWhere("orderAlias.store = :store");
  404.                 $validOrders->setParameter('store'$store);
  405.             }
  406.             $validOrders $validOrders->getQuery()->getSingleScalarResult();
  407.         $cancelledOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  408.             ->select('count(orderAlias.id)')
  409.             ->andWhere("orderAlias.status = 'cancelled' OR orderAlias.statusShipping = 'annulee'")
  410.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  411.             ->setParameter('start'$start)
  412.             ->setParameter('end'$end);
  413.             if($store){
  414.                 $cancelledOrders->andWhere("orderAlias.store = :store");
  415.                 $cancelledOrders->setParameter('store'$store);
  416.             }
  417.             $cancelledOrders $cancelledOrders->getQuery()->getSingleScalarResult();
  418.         $shippedOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  419.             ->select('count(orderAlias.id)')
  420.             ->andWhere("orderAlias.statusShipping = 'shipped'")
  421.             ->andWhere("orderAlias.status <> 'draft'")
  422.             ->andWhere("orderAlias.status <> 'cancelled'")
  423.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  424.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  425.             ->setParameter('start'$start)
  426.             ->setParameter('end'$end);
  427.             if($store){
  428.                 $shippedOrders->andWhere("orderAlias.store = :store");
  429.                 $shippedOrders->setParameter('store'$store);
  430.             }
  431.             $shippedOrders $shippedOrders->getQuery()->getSingleScalarResult();
  432.         $soldProducts =   $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  433.             ->select('sum(orderItems.quantity)')
  434.             ->leftJoin("orderAlias.orderItems""orderItems")
  435.             ->andWhere("orderAlias.status <> 'draft'")
  436.             ->andWhere("orderAlias.status <> 'cancelled'")
  437.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  438.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  439.             ->setParameter('start'$start)
  440.             ->setParameter('end'$end);
  441.             if($store){
  442.                 $soldProducts->andWhere("orderAlias.store = :store");
  443.                 $soldProducts->setParameter('store'$store);
  444.             }
  445.             $soldProducts $soldProducts->getQuery()->getSingleScalarResult();
  446.         $revenuedProducts =   $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  447.             ->select('sum(orderItems.quantity * orderItems.price)')
  448.             ->leftJoin("orderAlias.orderItems""orderItems")
  449.             ->andWhere("orderAlias.status <> 'draft'")
  450.             ->andWhere("orderAlias.status <> 'cancelled'")
  451.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  452.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  453.             ->setParameter('start'$start)
  454.             ->setParameter('end'$end);
  455.             if($store){
  456.                 $revenuedProducts->andWhere("orderAlias.store = :store");
  457.                 $revenuedProducts->setParameter('store'$store);
  458.             }
  459.             $revenuedProducts $revenuedProducts->getQuery()->getSingleScalarResult();
  460.         $averageOrderAmount $validOrders $revenuedProducts $validOrders 0;
  461.         //$inTypes = Order::IN_TYPES;
  462.         //$outTypes = Order::OUT_TYPES;
  463.         // $productInStock = $this->doctrine->createQueryBuilder()
  464.         //     ->select('p.id AS product_id, p.name AS product_name,p.initialStock AS initial_stock')
  465.         //     ->addSelect('
  466.         //         SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) AS stock_in,
  467.         //         SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END) AS stock_out,
  468.         //         (
  469.         //             p.initialStock +
  470.         //             SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) -
  471.         //             SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END)
  472.         //         ) AS current_stock
  473.         //     ')
  474.         //     ->from(Product::class, 'p')
  475.         //     ->leftJoin('p.orderItems', 'oi')
  476.         //     ->leftJoin('oi.parentOrder', 'o')
  477.         //     ->groupBy('p.id')
  478.         //     ->having('
  479.         //         (
  480.         //             p.initialStock +
  481.         //             SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) -
  482.         //             SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END)
  483.         //         ) > 0
  484.         //     ')
  485.         //     ->andWhere("o.status = '".Order::STATUS_VALIDATED."'")
  486.         //     //->andWhere('o.createdAt BETWEEN :start AND :end')
  487.         //     // ->setParameter('start', $start)
  488.         //     // ->setParameter('end', $end)
  489.         //     ->setMaxResults(16)
  490.         //     ->orderBy('current_stock', 'DESC')
  491.         //     ->getQuery()
  492.         //     ->getArrayResult();
  493.         $mostOrderedCategory $this->doctrine->getRepository(Order::class)
  494.             ->createQueryBuilder('o')
  495.             ->select('c.id, c.name as categoryName, COUNT(DISTINCT o.id) as orderCount')
  496.             ->join('o.orderItems''oi')
  497.             ->join('oi.product''p')
  498.             ->join('p.categoriesProduct''c'// Assuming products have a many-to-many relationship with categories
  499.             ->where("o.status NOT IN ('draft', 'cancelled') AND o.statusShipping <> 'annulee'")
  500.             ->andWhere('o.createdAt BETWEEN :start AND :end')
  501.             ->groupBy('c.id, c.name')
  502.             ->orderBy('orderCount''DESC')
  503.             ->setMaxResults(1// Get only the top category
  504.             ->setParameter('start'$start)
  505.             ->setParameter('end'$end);
  506.             if($store){
  507.                 $mostOrderedCategory->andWhere("o.store = :store");
  508.                 $mostOrderedCategory->setParameter('store'$store);
  509.             }
  510.             $mostOrderedCategory->getQuery()->getOneOrNullResult();
  511.             $mostOrderedCategory $mostOrderedCategory->getQuery()->getOneOrNullResult();
  512.                     
  513.         $mostOrderedCategoryName $mostOrderedCategory $mostOrderedCategory['categoryName'] : 'N/A';
  514.         // Get the most ordered product
  515.         $mostOrderedProduct $this->doctrine->getRepository(Order::class)
  516.             ->createQueryBuilder('o')
  517.             ->select('p.id, p.name as productName, SUM(oi.quantity) as totalQuantity, COUNT(DISTINCT o.id) as orderCount')
  518.             ->join('o.orderItems''oi')
  519.             ->join('oi.product''p')
  520.             ->where("o.status NOT IN ('draft', 'cancelled') AND o.statusShipping <> 'annulee'")
  521.             ->andWhere('o.createdAt BETWEEN :start AND :end')
  522.             ->groupBy('p.id, p.name')
  523.             ->orderBy('totalQuantity''DESC')
  524.             ->setMaxResults(1)
  525.             ->setParameter('start'$start)
  526.             ->setParameter('end'$end);
  527.             if($store){
  528.                 $mostOrderedProduct->andWhere("o.store = :store");
  529.                 $mostOrderedProduct->setParameter('store'$store);
  530.             }
  531.             $mostOrderedProduct $mostOrderedProduct->getQuery()->getOneOrNullResult();
  532.             
  533.         $mostOrderedProductName $mostOrderedProduct $mostOrderedProduct['productName'] : 'N/A';
  534.         // Payment status stats - separate queries for different payment statuses
  535.         // Paid orders: filter by paidAt date
  536.         $paidOrdersQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  537.             ->select('count(orderAlias.id)')
  538.             ->andWhere("orderAlias.status <> 'draft'")
  539.             ->andWhere("orderAlias.status <> 'cancelled'")
  540.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  541.             ->andWhere('orderAlias.paidAt BETWEEN :start AND :end')
  542.             ->setParameter('start'$start)
  543.             ->setParameter('end'$end);
  544.         if($store){
  545.             $paidOrdersQuery->andWhere("orderAlias.store = :store");
  546.             $paidOrdersQuery->setParameter('store'$store);
  547.         }
  548.         $paidOrders $paidOrdersQuery->getQuery()->getSingleScalarResult();
  549.         // Unpaid orders: filter by same date range as other stats
  550.         $unpaidOrdersQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  551.             ->select('count(orderAlias.id)')
  552.             ->andWhere("orderAlias.status <> 'draft'")
  553.             ->andWhere("orderAlias.status <> 'cancelled'")
  554.             ->andWhere("orderAlias.statusShipping = 'livree'")
  555.             ->andWhere('orderAlias.payedAmount = 0')
  556.             ->andWhere("orderAlias.status NOT IN ('paid', 'partially-paid')")
  557.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  558.             ->setParameter('start'$start)
  559.             ->setParameter('end'$end);
  560.         if($store){
  561.             $unpaidOrdersQuery->andWhere("orderAlias.store = :store");
  562.             $unpaidOrdersQuery->setParameter('store'$store);
  563.         }
  564.         $unpaidOrders $unpaidOrdersQuery->getQuery()->getSingleScalarResult();
  565.         // Calculate total amounts for paid orders (filtered by paidAt date)
  566.         $paidOrdersAmountQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  567.             ->select('sum(orderAlias.payedAmount)')
  568.             ->andWhere("orderAlias.status <> 'draft'")
  569.             ->andWhere("orderAlias.status <> 'cancelled'")
  570.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  571.             ->andWhere('orderAlias.paidAt BETWEEN :start AND :end')
  572.             ->setParameter('start'$start)
  573.             ->setParameter('end'$end);
  574.         if($store){
  575.             $paidOrdersAmountQuery->andWhere("orderAlias.store = :store");
  576.             $paidOrdersAmountQuery->setParameter('store'$store);
  577.         }
  578.         $paidOrdersAmount $paidOrdersAmountQuery->getQuery()->getSingleScalarResult() ?: 0;
  579.         // Calculate total amounts for unpaid orders (filtered by same date range)
  580.         $unpaidOrdersAmountQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  581.             ->select('sum(orderItems.quantity * orderItems.price)')
  582.             ->leftJoin("orderAlias.orderItems""orderItems")
  583.             ->andWhere("orderAlias.status <> 'draft'")
  584.             ->andWhere("orderAlias.status <> 'cancelled'")
  585.             ->andWhere("orderAlias.statusShipping = 'livree'")
  586.             ->andWhere('orderAlias.payedAmount = 0')
  587.             ->andWhere("orderAlias.status NOT IN ('paid', 'partially-paid')")
  588.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  589.             ->setParameter('start'$start)
  590.             ->setParameter('end'$end);
  591.         if($store){
  592.             $unpaidOrdersAmountQuery->andWhere("orderAlias.store = :store");
  593.             $unpaidOrdersAmountQuery->setParameter('store'$store);
  594.         }
  595.         $unpaidOrdersAmount $unpaidOrdersAmountQuery->getQuery()->getSingleScalarResult() ?: 0;
  596.         // Breakdown: paid amounts grouped by payment method
  597.         $paidByMethodQB $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  598.             ->select('pm.name AS methodName, pm.code AS methodCode, SUM(orderAlias.payedAmount) AS totalAmount')
  599.             ->leftJoin('orderAlias.paymentMethod''pm')
  600.             ->andWhere("orderAlias.status <> 'draft'")
  601.             ->andWhere("orderAlias.status <> 'cancelled'")
  602.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  603.             ->andWhere('orderAlias.paidAt BETWEEN :start AND :end')
  604.             ->groupBy('pm.id, pm.name, pm.code')
  605.             ->setParameter('start'$start)
  606.             ->setParameter('end'$end);
  607.         if ($store) {
  608.             $paidByMethodQB->andWhere('orderAlias.store = :store')->setParameter('store'$store);
  609.         }
  610.         $paidByMethod $paidByMethodQB->getQuery()->getArrayResult();
  611.         // Breakdown: unpaid amounts grouped by payment method
  612.         $unpaidByMethodQB $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  613.             ->select('pm.name AS methodName, pm.code AS methodCode, SUM(orderItems.quantity * orderItems.price) AS totalAmount')
  614.             ->leftJoin('orderAlias.paymentMethod''pm')
  615.             ->leftJoin('orderAlias.orderItems''orderItems')
  616.             ->andWhere("orderAlias.status <> 'draft'")
  617.             ->andWhere("orderAlias.status <> 'cancelled'")
  618.             ->andWhere("orderAlias.statusShipping = 'livree'")
  619.             ->andWhere('orderAlias.payedAmount = 0')
  620.             ->andWhere("orderAlias.status NOT IN ('paid', 'partially-paid')")
  621.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  622.             ->groupBy('pm.id, pm.name, pm.code')
  623.             ->setParameter('start'$start)
  624.             ->setParameter('end'$end);
  625.         if ($store) {
  626.             $unpaidByMethodQB->andWhere('orderAlias.store = :store')->setParameter('store'$store);
  627.         }
  628.         $unpaidByMethod $unpaidByMethodQB->getQuery()->getArrayResult();
  629.         $stats = [
  630.             [
  631.                 'label' => 'Chiffre d\'affaires',
  632.                 'value' => (float)round($revenuedProducts2),
  633.                 'icon' => 'fa-coins',
  634.                 'cssClass' => 'stat-revenue',
  635.                 'description' => 'Revenu total généré par les ventes',
  636.                 'url' => null,
  637.                 'unit' => 'MAD',
  638.             ],
  639.             [
  640.                 'label' => 'Ventes',
  641.                 'value' => (int)$validOrders,
  642.                 'icon' => 'fa-calendar-check',
  643.                 'cssClass' => 'stat-monthly-orders',
  644.                 'description' => 'Nombre de ventes validées',
  645.                 'url' => null,
  646.                 'unit' => null,
  647.             ],
  648.             [
  649.                 'label' => 'Produits vendus',
  650.                 'value' => (int)$soldProducts,
  651.                 'icon' => 'fa-boxes',
  652.                 'cssClass' => 'stat-products-sold',
  653.                 'description' => 'Nombre total de produits vendus',
  654.                 'url' => null,
  655.                 'unit' => null,
  656.             ],
  657.             [
  658.                 'label' => 'Panier moyen',
  659.                 'value' => (float)round($averageOrderAmount2),
  660.                 'icon' => 'fa-shopping-basket',
  661.                 'cssClass' => 'stat-average-basket',
  662.                 'description' => 'Montant moyen dépensé par commande',
  663.                 'url' => null,
  664.                 'unit' => 'MAD',
  665.             ],
  666.             [
  667.                 'label' => 'Commandes annulées',
  668.                 'value' => (float)$cancelledOrders,
  669.                 'icon' => 'fa-undo',
  670.                 'cssClass' => 'stat-refunds',
  671.                 'description' => 'Nombre de commandes annulées',
  672.                 'url' => null,
  673.                 'unit' => null,
  674.             ],
  675.             [
  676.                 'label' => 'Commandes livrées',
  677.                 'value' => (float)$shippedOrders,
  678.                 'icon' => 'fa-truck',
  679.                 'cssClass' => 'stat-delivered-orders',
  680.                 'description' => 'Nombre de commandes livrées',
  681.                 'url' => null,
  682.                 'unit' => null,
  683.             ],
  684.             [
  685.                 'label' => 'Catégorie phare',
  686.                 'value' => $mostOrderedCategoryName,
  687.                 'icon' => 'fa-star',
  688.                 'cssClass' => 'stat-top-category',
  689.                 'description' => 'Catégorie la plus vendue ce mois-ci',
  690.                 'url' => null,
  691.                 'unit' => null,
  692.             ],
  693.             [
  694.                 'label' => 'Produit phare',
  695.                 'value' => $mostOrderedProductName,
  696.                 'icon' => 'fa-star',
  697.                 'cssClass' => 'stat-top-product',
  698.                 'description' => 'Produit la plus vendu ce mois-ci',
  699.                 'url' => null,
  700.                 'unit' => null,
  701.             ],
  702.             [
  703.                 'label' => 'Commandes réglées',
  704.                 'value' => $paidOrders,
  705.                 'amount' => (float)$paidOrdersAmount,
  706.                 'icon' => 'fa-check-circle',
  707.                 'cssClass' => 'stat-paid-orders',
  708.                 'description' => 'Nombre de commandes entièrement réglées',
  709.                 'url' => null,
  710.                 'unit' => null,
  711.                 'breakdown' => array_map(function(array $row) { return ['label' => $row['methodName'] ?? '—''amount' => (float)($row['totalAmount'] ?? 0)]; }, $paidByMethod),
  712.             ],
  713.             [
  714.                 'label' => 'Commandes à régler',
  715.                 'value' => $unpaidOrders,
  716.                 'amount' => (float)$unpaidOrdersAmount,
  717.                 'icon' => 'fa-clock',
  718.                 'cssClass' => 'stat-unpaid-orders',
  719.                 'description' => 'Nombre de commandes en attente de réglement',
  720.                 'url' => null,
  721.                 'unit' => null,
  722.                 'breakdown' => array_map(function(array $row) { return ['label' => $row['methodName'] ?? '—''amount' => (float)($row['totalAmount'] ?? 0)]; }, $unpaidByMethod),
  723.             ],
  724.         ];
  725.         $productLabels = [];
  726.         $productChartData = [];
  727.         // foreach ($productInStock as $product) {
  728.         //     $productLabels[] = $product['product_name'];
  729.         //     $productChartData[] = (float) $product['current_stock']; // cast to float for Chart.js
  730.         // }
  731.         // $productChartData = [
  732.         //     'labels' => $productLabels,
  733.         //     'data' => $productChartData,
  734.         // ];
  735.         return $this->render(
  736.             "bundles/EasyAdminBundle/welcome.html.twig",
  737.             [
  738.                 'stores' => $stores,
  739.                 'store' => $store,
  740.                 'stats' => $stats,
  741.                 'productChartData' => $productChartData,
  742.                 'queryParams' => $queryParams,
  743.             ]
  744.         );
  745.     }
  746.     public function configureCrud(): Crud
  747.     {
  748.         return
  749.             Crud::new()
  750.             ->setDefaultSort(["id" => 'DESC'])
  751.             ->setPaginatorPageSize(12)
  752.         ;
  753.     }
  754.     public function configureDashboard(): Dashboard
  755.     {
  756.         $mainSettings $this->doctrine->getManager()->getRepository(Settings::class)->findOneBy(["code" => "main"]);
  757.         $nameSpaceTrans strtolower($mainSettings->getProjectName()) . "-admin";
  758.         $urlImage "../themes/" strtolower($mainSettings->getAssetFolderName()) . "/admin/images/logo.png";
  759.         return Dashboard::new()
  760.             ->setTitle('<img title="Dashboard" src="' $urlImage '" />
  761.             
  762.             ')
  763.             //->renderContentMaximized()
  764.             ->setFaviconPath("../themes/" strtolower($mainSettings->getAssetFolderName()) . "/admin/images/icon.png")
  765.             ->setTranslationDomain($nameSpaceTrans)
  766.             ->disableUrlSignatures()
  767.             ->setLocales(
  768.                 [
  769.                     'fr' => 'Français',
  770.                     'en' => 'English',
  771.                     'ar' => 'Arabe',
  772.                 ]
  773.             )
  774.         ;
  775.     }
  776.     public function configureMenuItems(): iterable
  777.     {
  778.         /* START : Les Extensions IlaveU */
  779.         $applications $this->doctrine->getManager()->getRepository(Application::class)->findBy(["isEnabled" => true], ["menuOrder" => "ASC"]);
  780.         $settings $this->doctrine->getManager()->getRepository(Settings::class)->findOneBy(["code" => "main"]);
  781.         //$finder = new Finder();
  782.         $filesystem = new Filesystem();
  783.         //$finder->directories()->in(__DIR__."/../../IlaveU")->depth('== 0');
  784.         // For Principal Bundles (ShopBundle + FrontBundle + ...) 
  785.         foreach ($applications as $singleApplication) {
  786.             $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/" $singleApplication->getName() . "/IlaveU" $singleApplication->getName() . ".php");
  787.             if (!$bundleExist) {
  788.                 continue;
  789.             }
  790.             $bundleName $singleApplication->getName();
  791.             // Les themes systemes IlaveU (FrontBundle Themes)
  792.             if ($bundleName == "FrontBundle") {
  793.                 $bundleDashboardController 'App\IlaveU\FrontBundle\Themes\\' $settings->getFrontTheme() . '\Controller\DashboardController';
  794.             } else {
  795.                 $bundleDashboardController 'App\IlaveU\\' $bundleName '\Controller\DashboardController';
  796.             }
  797.             $dashboard = new $bundleDashboardController();
  798.             foreach ($dashboard->configureMenuItems() as $menu) {
  799.                 yield $menu;
  800.             }
  801.         }
  802.         // For Additional Apps Bundles (POSBundle + OtherBundle + ...)
  803.         foreach ($applications as $singleApplication) {
  804.             if ($singleApplication->getParentApplication()) {
  805.                 continue;
  806.             }
  807.             $menuArray = [];
  808.             $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/Apps/" $singleApplication->getName() . "/IlaveU" $singleApplication->getName() . ".php");
  809.             if (!$bundleExist) {
  810.                 continue;
  811.             }
  812.             $bundleName $singleApplication->getName();
  813.             $bundleDashboardController 'App\IlaveU\Apps\\' $bundleName '\Controller\DashboardController';
  814.             $dashboard = new $bundleDashboardController();
  815.             foreach ($dashboard->configureMenuItems() as $menu) {
  816.                 yield $menu;
  817.             }
  818.             //SubApplications 
  819.             foreach ($singleApplication->getSubApplications() as $subApplication) {
  820.                 $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/Apps/" $subApplication->getName() . "/IlaveU" $subApplication->getName() . ".php");
  821.                 if (!$bundleExist) {
  822.                     continue;
  823.                 }
  824.                 $bundleName $subApplication->getName();
  825.                 $bundleDashboardController 'App\IlaveU\Apps\\' $bundleName '\Controller\DashboardController';
  826.                 $dashboard = new $bundleDashboardController();
  827.                 foreach ($dashboard->configureMenuItems() as $menu) {
  828.                     yield $menu;
  829.                 }
  830.             }
  831.         }
  832.         /* END : Les Extensions IlaveU */
  833.         yield MenuItem::section('Commandes');
  834.         
  835.         yield MenuItem::linkToCrud('Commandes réglées''fas fa-check-circle'Order::class)
  836.             ->setController(PaidOrdersCrudController::class);
  837.         yield MenuItem::linkToCrud('Commandes à payer''fas fa-clock'Order::class)
  838.             ->setController(UnpaidOrdersCrudController::class);
  839.         yield MenuItem::section('Parametres');
  840.         yield MenuItem::linkToRoute('Theme Designer''fas fa-shield-alt'"website_theme_grapesjs_edit")->setPermission("ROLE_ADMIN_DEV");
  841.         yield MenuItem::linkToRoute('Apps Store''fas fa-shield-alt'"app_store")->setPermission("ROLE_ADMIN_DEV");
  842.         yield MenuItem::linkToCrud('Utilisateurs''fas fa-shield-alt'User::class)->setController(UserCrudController::class);
  843.         yield MenuItem::linkToCrud('Passwords''fas fa-shield-alt'User::class)->setController(UserPasswordCrudController::class);
  844.         yield MenuItem::linkToCrud('Roles''fas fa-shield-alt'Role::class);
  845.         yield MenuItem::linkToCrud('LinkType''fas fa-gears'LinkType::class)->setPermission("ROLE_ADMIN_DEV");
  846.         yield MenuItem::linkToCrud('Link''fas fa-gears'Link::class)->setPermission("ROLE_ADMIN_DEV");
  847.         yield MenuItem::linkToCrud('Applications''fas fa-shield-alt'Application::class)->setPermission("ROLE_ADMIN_DEV");
  848.         yield MenuItem::linkToCrud('Notifications''fas fa-shield-alt'Notification::class)->setPermission("ROLE_ADMIN_DEV");
  849.         yield MenuItem::linkToRoute('Push mobile (tous)''fas fa-mobile-screen-button''admin_push_broadcast')->setPermission("ROLE_ADMIN_DEV");
  850.         yield MenuItem::linkToRoute('Push mobile (un client)''fas fa-user''admin_push_targeted')->setPermission("ROLE_ADMIN_DEV");
  851.         yield MenuItem::linkToRoute('Push mobile (un groupe)''fas fa-users''admin_push_group')->setPermission("ROLE_ADMIN_DEV");
  852.         yield MenuItem::linkToRoute('Text to speech''fas fa-shield-alt'"open_ai_tts")->setPermission("ROLE_ADMIN_DEV");
  853.         yield MenuItem::linkToCrud('Historique''fas fa-shield-alt'EntityLog::class)->setPermission("ROLE_ADMIN");
  854.         yield MenuItem::linkToCrud('Settings''fas fa-shield-alt'Settings::class)
  855.             ->setAction("edit")
  856.             ->setEntityId($settings->getId());
  857.         yield MenuItem::linkToCrud('Currency Exchange''fas fa-shield-alt'CurrencyExchangeRate::class);
  858.         yield MenuItem::linkToCrud('Web site theme''fas fa-shield-alt'WebsiteTheme::class)->setPermission("ROLE_ADMIN_DEV");
  859.         yield MenuItem::linkToCrud('Front Themes''fas fa-shield-alt'FrontTheme::class)->setPermission("ROLE_ADMIN_DEV");
  860.         yield MenuItem::linkToCrud('Export Excel''fas fa-shield-alt'ExportExcel::class)->setController(ExportExcelCrudController::class)->setPermission("ROLE_ADMIN_DEV");
  861.     }
  862. }