src/Controller/Admin/DashboardController.php line 673

  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\IlaveU\ShopBundle\Entity\Order\Order;
  24. use App\IlaveU\ShopBundle\Entity\Product\Product;
  25. use App\IlaveU\ShopBundle\Entity\Store\Store;
  26. use App\IlaveU\ShopBundle\Entity\Vendor\Vendor;
  27. use App\IlaveU\ShopBundle\Service\IlaveUShopStatisticProvider;
  28. use App\Repository\ApplicationRepository;
  29. use App\Service\IlaveUSettingsProvider;
  30. use Doctrine\Persistence\ManagerRegistry;
  31. use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
  32. use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
  33. use Symfony\Component\Filesystem\Filesystem;
  34. use Symfony\Component\Finder\Finder;
  35. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  36. use App\Form\Admin\BroadcastPushNotificationType;
  37. use App\Repository\UserRepository;
  38. use App\Service\ExpoPushNotificationService;
  39. use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
  40. use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
  41. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityRepositoryInterface;
  42. use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
  43. use Symfony\Component\HttpFoundation\Request;
  44. use Symfony\Component\HttpFoundation\RequestStack;
  45. class DashboardController extends AbstractDashboardController
  46. {
  47.     public function __construct(
  48.         private readonly ManagerRegistry $doctrine,
  49.         private readonly IlaveUShopStatisticProvider $ilaveShopStatisticProvider,
  50.         private readonly RequestStack $requestStack,
  51.     ) {}
  52.     #[Route(path'/admin'name'admin_non_locale')]
  53.     public function indexNonLocale(Request $request): Response
  54.     {
  55.         return $this->redirectToRoute("admin", ["_locale" => $request->getLocale()]);
  56.     }
  57.     #[Route('/{_locale}/admin/website-theme/grapesjs_edit'name'website_theme_grapesjs_edit')]
  58.     public function website_theme_grapesjs_edit(): Response
  59.     {
  60.         return $this->render('@IlaveU/FrontBundle/Themes/' $this->container->get('twig')->getGlobals()["settings"]->get()->getAssetFolderName() . '/templates/admin/website-theme/grapesjs.html.twig');
  61.     }
  62.     
  63.     
  64.     #[Route(path'/{_locale}/admin/push-notifications/broadcast'name'admin_push_broadcast')]
  65.     #[IsGranted('ROLE_ADMIN_DEV')]
  66.     public function pushBroadcast(
  67.         Request $request,
  68.         UserRepository $userRepository,
  69.         ExpoPushNotificationService $expoPush,
  70.     ): Response {
  71.         $recipientCount $userRepository->countUsersWithExponentPushToken();
  72.         $tokens $userRepository->findDistinctExponentPushTokens();
  73.         $form $this->createForm(BroadcastPushNotificationType::class);
  74.         $form->handleRequest($request);
  75.         if ($form->isSubmitted() && $form->isValid()) {
  76.             $data $form->getData();
  77.             $title = (string) ($data['title'] ?? '');
  78.             $message = (string) ($data['message'] ?? '');
  79.             $result $expoPush->sendToTokens($tokens$title$message);
  80.             if ($result['sent'] > 0) {
  81.                 $this->addFlash('success'sprintf(
  82.                     'Notifications envoyées : %d succès.',
  83.                     $result['sent']
  84.                 ));
  85.             }
  86.             if ($result['failed'] > 0) {
  87.                 $this->addFlash('warning'sprintf(
  88.                     'Échecs partiels : %d envoi(s) en erreur.',
  89.                     $result['failed']
  90.                 ));
  91.             }
  92.             foreach (array_slice($result['errors'], 05) as $err) {
  93.                 $this->addFlash('danger'$err);
  94.             }
  95.             if ($result['sent'] === && $result['failed'] === && $tokens === []) {
  96.                 $this->addFlash('warning''Aucun utilisateur avec un token Expo enregistré.');
  97.             }
  98.             return $this->redirectToRoute('admin_push_broadcast', ['_locale' => $request->getLocale()]);
  99.         }
  100.         return $this->render('admin/push-broadcast.html.twig', [
  101.             'form' => $form->createView(),
  102.             'recipientCount' => $recipientCount,
  103.         ]);
  104.     }
  105.     #[Route(path'/{_locale}/admin/apps-store'name'app_store')]
  106.     public function appStore(Request $requestApplicationRepository $applicationRepositoryIlaveUSettingsProvider $ilaveSettingsProvider): Response
  107.     {
  108.         $appsArray = [];
  109.         foreach ($applicationRepository->findAll() as $singleApp) {
  110.             $appsArray[] = [
  111.                 "id" => $singleApp->getId(),
  112.                 "name" => $singleApp->getName(),
  113.                 "image" => $singleApp->getImage(),
  114.                 "price" => $singleApp->getPrice(),
  115.                 "pageUrl" => $singleApp->getPageUrl(),
  116.             ];
  117.         }
  118.         return $this->render("admin/app-store.html.twig", [
  119.             "installedApps" => $appsArray,
  120.             "JWT" => $ilaveSettingsProvider->createJWTForUser($this->getUser())
  121.         ]);
  122.     }
  123.     #[Route(path'/{_locale}/admin'name'admin')]
  124.     public function index(): Response
  125.     {
  126.         $url $this->requestStack->getCurrentRequest()->server->get('REQUEST_URI');
  127.         
  128.         $parsedUrl parse_url($url);
  129.         parse_str($parsedUrl['query'] ?? ''$queryParams);
  130.         if (count($queryParams) == 0) {
  131.             $queryParams['store'] = null;
  132.             $queryParams['range'] = null;
  133.             $queryParams['from'] = null;
  134.             $queryParams['to'] = null;
  135.         }
  136.         $range   $queryParams['range'] ?? 'today';
  137.         $from    $queryParams['from'] ?? null;
  138.         $to      $queryParams['to'] ?? null;
  139.         $storeParam   = (int)$queryParams['store'] ?? null;
  140.         $store null;
  141.         
  142.         
  143.         // Check if user has ROLE_STORE and get their store
  144.         if ($this->isGranted('ROLE_STORE')) {
  145.             $currentUser $this->getUser();
  146.             if ($currentUser) {
  147.                 $storeEntity $this->doctrine->getRepository(Store::class)->findOneBy(["user" => $currentUser]);
  148.                 $store $storeEntity;
  149.             }
  150.         } elseif ($storeParam != 0) {
  151.             $storeEntity $this->doctrine->getRepository(Store::class)->findOneBy(["id" => $storeParam]);
  152.             $store $storeEntity;
  153.         }
  154.         
  155.         
  156.         //dd($request);
  157.         // Get list of stores
  158.         $stores $this->doctrine->getRepository(Store::class)->findAll();
  159.         // Handle predefined ranges
  160.         $now = new \DateTimeImmutable();
  161.         // If custom date range provided, it overrides predefined range
  162.         if ($from && $to) {
  163.             $start \DateTimeImmutable::createFromFormat('Y-m-d'$from)?->setTime(00);
  164.             $end   \DateTimeImmutable::createFromFormat('Y-m-d'$to)?->setTime(235959);
  165.         } elseif ($range === 'today') {
  166.             $start $now->modify('today')->setTime(00);
  167.             $end $now->modify('today')->setTime(235959);
  168.         } elseif ($range === 'yesterday') {
  169.             $start $now->modify('yesterday')->setTime(00);
  170.             $end $now->modify('yesterday')->setTime(235959);
  171.         } elseif ($range === 'week') {
  172.             $start $now->modify('this week')->setTime(00);
  173.             $end $now->modify('next week')->setTime(00)->modify('-1 second');
  174.         } elseif ($range === 'month') {
  175.             $start $now->modify('first day of this month')->setTime(00);
  176.             $end $now->modify('last day of this month')->setTime(235959);
  177.         } elseif ($range === 'year') {
  178.             $start $now->modify('first day of January')->setTime(00);
  179.             $end $now->modify('last day of December')->setTime(235959);
  180.         } else { // Month By Default
  181.             $start $now->modify('today')->setTime(00);
  182.             $end $now->modify('today')->setTime(235959);
  183.         }
  184.         // If custom date range provided (overrides predefined range)
  185.         
  186.         $validOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  187.             ->select('count(orderAlias.id)')
  188.             ->andWhere("orderAlias.status <> 'draft'")
  189.             ->andWhere("orderAlias.status <> 'cancelled'")
  190.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  191.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  192.             ->setParameter('start'$start)
  193.             ->setParameter('end'$end);
  194.             if($store){
  195.                 $validOrders->andWhere("orderAlias.store = :store");
  196.                 $validOrders->setParameter('store'$store);
  197.             }
  198.             $validOrders $validOrders->getQuery()->getSingleScalarResult();
  199.         $cancelledOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  200.             ->select('count(orderAlias.id)')
  201.             ->andWhere("orderAlias.status = 'cancelled' OR orderAlias.statusShipping = 'annulee'")
  202.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  203.             ->setParameter('start'$start)
  204.             ->setParameter('end'$end);
  205.             if($store){
  206.                 $cancelledOrders->andWhere("orderAlias.store = :store");
  207.                 $cancelledOrders->setParameter('store'$store);
  208.             }
  209.             $cancelledOrders $cancelledOrders->getQuery()->getSingleScalarResult();
  210.         $shippedOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  211.             ->select('count(orderAlias.id)')
  212.             ->andWhere("orderAlias.statusShipping = 'shipped'")
  213.             ->andWhere("orderAlias.status <> 'draft'")
  214.             ->andWhere("orderAlias.status <> 'cancelled'")
  215.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  216.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  217.             ->setParameter('start'$start)
  218.             ->setParameter('end'$end);
  219.             if($store){
  220.                 $shippedOrders->andWhere("orderAlias.store = :store");
  221.                 $shippedOrders->setParameter('store'$store);
  222.             }
  223.             $shippedOrders $shippedOrders->getQuery()->getSingleScalarResult();
  224.         $soldProducts =   $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  225.             ->select('sum(orderItems.quantity)')
  226.             ->leftJoin("orderAlias.orderItems""orderItems")
  227.             ->andWhere("orderAlias.status <> 'draft'")
  228.             ->andWhere("orderAlias.status <> 'cancelled'")
  229.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  230.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  231.             ->setParameter('start'$start)
  232.             ->setParameter('end'$end);
  233.             if($store){
  234.                 $soldProducts->andWhere("orderAlias.store = :store");
  235.                 $soldProducts->setParameter('store'$store);
  236.             }
  237.             $soldProducts $soldProducts->getQuery()->getSingleScalarResult();
  238.         $revenuedProducts =   $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  239.             ->select('sum(orderItems.quantity * orderItems.price)')
  240.             ->leftJoin("orderAlias.orderItems""orderItems")
  241.             ->andWhere("orderAlias.status <> 'draft'")
  242.             ->andWhere("orderAlias.status <> 'cancelled'")
  243.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  244.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  245.             ->setParameter('start'$start)
  246.             ->setParameter('end'$end);
  247.             if($store){
  248.                 $revenuedProducts->andWhere("orderAlias.store = :store");
  249.                 $revenuedProducts->setParameter('store'$store);
  250.             }
  251.             $revenuedProducts $revenuedProducts->getQuery()->getSingleScalarResult();
  252.         $averageOrderAmount $validOrders $revenuedProducts $validOrders 0;
  253.         //$inTypes = Order::IN_TYPES;
  254.         //$outTypes = Order::OUT_TYPES;
  255.         // $productInStock = $this->doctrine->createQueryBuilder()
  256.         //     ->select('p.id AS product_id, p.name AS product_name,p.initialStock AS initial_stock')
  257.         //     ->addSelect('
  258.         //         SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) AS stock_in,
  259.         //         SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END) AS stock_out,
  260.         //         (
  261.         //             p.initialStock +
  262.         //             SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) -
  263.         //             SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END)
  264.         //         ) AS current_stock
  265.         //     ')
  266.         //     ->from(Product::class, 'p')
  267.         //     ->leftJoin('p.orderItems', 'oi')
  268.         //     ->leftJoin('oi.parentOrder', 'o')
  269.         //     ->groupBy('p.id')
  270.         //     ->having('
  271.         //         (
  272.         //             p.initialStock +
  273.         //             SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) -
  274.         //             SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END)
  275.         //         ) > 0
  276.         //     ')
  277.         //     ->andWhere("o.status = '".Order::STATUS_VALIDATED."'")
  278.         //     //->andWhere('o.createdAt BETWEEN :start AND :end')
  279.         //     // ->setParameter('start', $start)
  280.         //     // ->setParameter('end', $end)
  281.         //     ->setMaxResults(16)
  282.         //     ->orderBy('current_stock', 'DESC')
  283.         //     ->getQuery()
  284.         //     ->getArrayResult();
  285.         $mostOrderedCategory $this->doctrine->getRepository(Order::class)
  286.             ->createQueryBuilder('o')
  287.             ->select('c.id, c.name as categoryName, COUNT(DISTINCT o.id) as orderCount')
  288.             ->join('o.orderItems''oi')
  289.             ->join('oi.product''p')
  290.             ->join('p.categoriesProduct''c'// Assuming products have a many-to-many relationship with categories
  291.             ->where("o.status NOT IN ('draft', 'cancelled') AND o.statusShipping <> 'annulee'")
  292.             ->andWhere('o.createdAt BETWEEN :start AND :end')
  293.             ->groupBy('c.id, c.name')
  294.             ->orderBy('orderCount''DESC')
  295.             ->setMaxResults(1// Get only the top category
  296.             ->setParameter('start'$start)
  297.             ->setParameter('end'$end);
  298.             if($store){
  299.                 $mostOrderedCategory->andWhere("o.store = :store");
  300.                 $mostOrderedCategory->setParameter('store'$store);
  301.             }
  302.             $mostOrderedCategory->getQuery()->getOneOrNullResult();
  303.             $mostOrderedCategory $mostOrderedCategory->getQuery()->getOneOrNullResult();
  304.                     
  305.         $mostOrderedCategoryName $mostOrderedCategory $mostOrderedCategory['categoryName'] : 'N/A';
  306.         // Get the most ordered product
  307.         $mostOrderedProduct $this->doctrine->getRepository(Order::class)
  308.             ->createQueryBuilder('o')
  309.             ->select('p.id, p.name as productName, SUM(oi.quantity) as totalQuantity, COUNT(DISTINCT o.id) as orderCount')
  310.             ->join('o.orderItems''oi')
  311.             ->join('oi.product''p')
  312.             ->where("o.status NOT IN ('draft', 'cancelled') AND o.statusShipping <> 'annulee'")
  313.             ->andWhere('o.createdAt BETWEEN :start AND :end')
  314.             ->groupBy('p.id, p.name')
  315.             ->orderBy('totalQuantity''DESC')
  316.             ->setMaxResults(1)
  317.             ->setParameter('start'$start)
  318.             ->setParameter('end'$end);
  319.             if($store){
  320.                 $mostOrderedProduct->andWhere("o.store = :store");
  321.                 $mostOrderedProduct->setParameter('store'$store);
  322.             }
  323.             $mostOrderedProduct $mostOrderedProduct->getQuery()->getOneOrNullResult();
  324.             
  325.         $mostOrderedProductName $mostOrderedProduct $mostOrderedProduct['productName'] : 'N/A';
  326.         // Payment status stats - separate queries for different payment statuses
  327.         // Paid orders: filter by paidAt date
  328.         $paidOrdersQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  329.             ->select('count(orderAlias.id)')
  330.             ->andWhere("orderAlias.status <> 'draft'")
  331.             ->andWhere("orderAlias.status <> 'cancelled'")
  332.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  333.             ->andWhere('orderAlias.paidAt BETWEEN :start AND :end')
  334.             ->setParameter('start'$start)
  335.             ->setParameter('end'$end);
  336.         if($store){
  337.             $paidOrdersQuery->andWhere("orderAlias.store = :store");
  338.             $paidOrdersQuery->setParameter('store'$store);
  339.         }
  340.         $paidOrders $paidOrdersQuery->getQuery()->getSingleScalarResult();
  341.         // Unpaid orders: filter by same date range as other stats
  342.         $unpaidOrdersQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  343.             ->select('count(orderAlias.id)')
  344.             ->andWhere("orderAlias.status <> 'draft'")
  345.             ->andWhere("orderAlias.status <> 'cancelled'")
  346.             ->andWhere("orderAlias.statusShipping = 'livree'")
  347.             ->andWhere('orderAlias.payedAmount = 0')
  348.             ->andWhere("orderAlias.status NOT IN ('paid', 'partially-paid')")
  349.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  350.             ->setParameter('start'$start)
  351.             ->setParameter('end'$end);
  352.         if($store){
  353.             $unpaidOrdersQuery->andWhere("orderAlias.store = :store");
  354.             $unpaidOrdersQuery->setParameter('store'$store);
  355.         }
  356.         $unpaidOrders $unpaidOrdersQuery->getQuery()->getSingleScalarResult();
  357.         // Calculate total amounts for paid orders (filtered by paidAt date)
  358.         $paidOrdersAmountQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  359.             ->select('sum(orderAlias.payedAmount)')
  360.             ->andWhere("orderAlias.status <> 'draft'")
  361.             ->andWhere("orderAlias.status <> 'cancelled'")
  362.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  363.             ->andWhere('orderAlias.paidAt BETWEEN :start AND :end')
  364.             ->setParameter('start'$start)
  365.             ->setParameter('end'$end);
  366.         if($store){
  367.             $paidOrdersAmountQuery->andWhere("orderAlias.store = :store");
  368.             $paidOrdersAmountQuery->setParameter('store'$store);
  369.         }
  370.         $paidOrdersAmount $paidOrdersAmountQuery->getQuery()->getSingleScalarResult() ?: 0;
  371.         // Calculate total amounts for unpaid orders (filtered by same date range)
  372.         $unpaidOrdersAmountQuery $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  373.             ->select('sum(orderItems.quantity * orderItems.price)')
  374.             ->leftJoin("orderAlias.orderItems""orderItems")
  375.             ->andWhere("orderAlias.status <> 'draft'")
  376.             ->andWhere("orderAlias.status <> 'cancelled'")
  377.             ->andWhere("orderAlias.statusShipping = 'livree'")
  378.             ->andWhere('orderAlias.payedAmount = 0')
  379.             ->andWhere("orderAlias.status NOT IN ('paid', 'partially-paid')")
  380.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  381.             ->setParameter('start'$start)
  382.             ->setParameter('end'$end);
  383.         if($store){
  384.             $unpaidOrdersAmountQuery->andWhere("orderAlias.store = :store");
  385.             $unpaidOrdersAmountQuery->setParameter('store'$store);
  386.         }
  387.         $unpaidOrdersAmount $unpaidOrdersAmountQuery->getQuery()->getSingleScalarResult() ?: 0;
  388.         // Breakdown: paid amounts grouped by payment method
  389.         $paidByMethodQB $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  390.             ->select('pm.name AS methodName, pm.code AS methodCode, SUM(orderAlias.payedAmount) AS totalAmount')
  391.             ->leftJoin('orderAlias.paymentMethod''pm')
  392.             ->andWhere("orderAlias.status <> 'draft'")
  393.             ->andWhere("orderAlias.status <> 'cancelled'")
  394.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  395.             ->andWhere('orderAlias.paidAt BETWEEN :start AND :end')
  396.             ->groupBy('pm.id, pm.name, pm.code')
  397.             ->setParameter('start'$start)
  398.             ->setParameter('end'$end);
  399.         if ($store) {
  400.             $paidByMethodQB->andWhere('orderAlias.store = :store')->setParameter('store'$store);
  401.         }
  402.         $paidByMethod $paidByMethodQB->getQuery()->getArrayResult();
  403.         // Breakdown: unpaid amounts grouped by payment method
  404.         $unpaidByMethodQB $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  405.             ->select('pm.name AS methodName, pm.code AS methodCode, SUM(orderItems.quantity * orderItems.price) AS totalAmount')
  406.             ->leftJoin('orderAlias.paymentMethod''pm')
  407.             ->leftJoin('orderAlias.orderItems''orderItems')
  408.             ->andWhere("orderAlias.status <> 'draft'")
  409.             ->andWhere("orderAlias.status <> 'cancelled'")
  410.             ->andWhere("orderAlias.statusShipping = 'livree'")
  411.             ->andWhere('orderAlias.payedAmount = 0')
  412.             ->andWhere("orderAlias.status NOT IN ('paid', 'partially-paid')")
  413.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  414.             ->groupBy('pm.id, pm.name, pm.code')
  415.             ->setParameter('start'$start)
  416.             ->setParameter('end'$end);
  417.         if ($store) {
  418.             $unpaidByMethodQB->andWhere('orderAlias.store = :store')->setParameter('store'$store);
  419.         }
  420.         $unpaidByMethod $unpaidByMethodQB->getQuery()->getArrayResult();
  421.         $stats = [
  422.             [
  423.                 'label' => 'Chiffre d\'affaires',
  424.                 'value' => (float)round($revenuedProducts2),
  425.                 'icon' => 'fa-coins',
  426.                 'cssClass' => 'stat-revenue',
  427.                 'description' => 'Revenu total généré par les ventes',
  428.                 'url' => null,
  429.                 'unit' => 'MAD',
  430.             ],
  431.             [
  432.                 'label' => 'Ventes',
  433.                 'value' => (int)$validOrders,
  434.                 'icon' => 'fa-calendar-check',
  435.                 'cssClass' => 'stat-monthly-orders',
  436.                 'description' => 'Nombre de ventes validées',
  437.                 'url' => null,
  438.                 'unit' => null,
  439.             ],
  440.             [
  441.                 'label' => 'Produits vendus',
  442.                 'value' => (int)$soldProducts,
  443.                 'icon' => 'fa-boxes',
  444.                 'cssClass' => 'stat-products-sold',
  445.                 'description' => 'Nombre total de produits vendus',
  446.                 'url' => null,
  447.                 'unit' => null,
  448.             ],
  449.             [
  450.                 'label' => 'Panier moyen',
  451.                 'value' => (float)round($averageOrderAmount2),
  452.                 'icon' => 'fa-shopping-basket',
  453.                 'cssClass' => 'stat-average-basket',
  454.                 'description' => 'Montant moyen dépensé par commande',
  455.                 'url' => null,
  456.                 'unit' => 'MAD',
  457.             ],
  458.             [
  459.                 'label' => 'Commandes annulées',
  460.                 'value' => (float)$cancelledOrders,
  461.                 'icon' => 'fa-undo',
  462.                 'cssClass' => 'stat-refunds',
  463.                 'description' => 'Nombre de commandes annulées',
  464.                 'url' => null,
  465.                 'unit' => null,
  466.             ],
  467.             [
  468.                 'label' => 'Commandes livrées',
  469.                 'value' => (float)$shippedOrders,
  470.                 'icon' => 'fa-truck',
  471.                 'cssClass' => 'stat-delivered-orders',
  472.                 'description' => 'Nombre de commandes livrées',
  473.                 'url' => null,
  474.                 'unit' => null,
  475.             ],
  476.             [
  477.                 'label' => 'Catégorie phare',
  478.                 'value' => $mostOrderedCategoryName,
  479.                 'icon' => 'fa-star',
  480.                 'cssClass' => 'stat-top-category',
  481.                 'description' => 'Catégorie la plus vendue ce mois-ci',
  482.                 'url' => null,
  483.                 'unit' => null,
  484.             ],
  485.             [
  486.                 'label' => 'Produit phare',
  487.                 'value' => $mostOrderedProductName,
  488.                 'icon' => 'fa-star',
  489.                 'cssClass' => 'stat-top-product',
  490.                 'description' => 'Produit la plus vendu ce mois-ci',
  491.                 'url' => null,
  492.                 'unit' => null,
  493.             ],
  494.             [
  495.                 'label' => 'Commandes réglées',
  496.                 'value' => $paidOrders,
  497.                 'amount' => (float)$paidOrdersAmount,
  498.                 'icon' => 'fa-check-circle',
  499.                 'cssClass' => 'stat-paid-orders',
  500.                 'description' => 'Nombre de commandes entièrement réglées',
  501.                 'url' => null,
  502.                 'unit' => null,
  503.                 'breakdown' => array_map(function(array $row) { return ['label' => $row['methodName'] ?? '—''amount' => (float)($row['totalAmount'] ?? 0)]; }, $paidByMethod),
  504.             ],
  505.             [
  506.                 'label' => 'Commandes à régler',
  507.                 'value' => $unpaidOrders,
  508.                 'amount' => (float)$unpaidOrdersAmount,
  509.                 'icon' => 'fa-clock',
  510.                 'cssClass' => 'stat-unpaid-orders',
  511.                 'description' => 'Nombre de commandes en attente de réglement',
  512.                 'url' => null,
  513.                 'unit' => null,
  514.                 'breakdown' => array_map(function(array $row) { return ['label' => $row['methodName'] ?? '—''amount' => (float)($row['totalAmount'] ?? 0)]; }, $unpaidByMethod),
  515.             ],
  516.         ];
  517.         $productLabels = [];
  518.         $productChartData = [];
  519.         // foreach ($productInStock as $product) {
  520.         //     $productLabels[] = $product['product_name'];
  521.         //     $productChartData[] = (float) $product['current_stock']; // cast to float for Chart.js
  522.         // }
  523.         // $productChartData = [
  524.         //     'labels' => $productLabels,
  525.         //     'data' => $productChartData,
  526.         // ];
  527.         return $this->render(
  528.             "bundles/EasyAdminBundle/welcome.html.twig",
  529.             [
  530.                 'stores' => $stores,
  531.                 'store' => $store,
  532.                 'stats' => $stats,
  533.                 'productChartData' => $productChartData,
  534.                 'queryParams' => $queryParams,
  535.             ]
  536.         );
  537.     }
  538.     public function configureCrud(): Crud
  539.     {
  540.         return
  541.             Crud::new()
  542.             ->setDefaultSort(["id" => 'DESC'])
  543.             ->setPaginatorPageSize(12)
  544.         ;
  545.     }
  546.     public function configureDashboard(): Dashboard
  547.     {
  548.         $mainSettings $this->doctrine->getManager()->getRepository(Settings::class)->findOneBy(["code" => "main"]);
  549.         $nameSpaceTrans strtolower($mainSettings->getProjectName()) . "-admin";
  550.         $urlImage "../themes/" strtolower($mainSettings->getAssetFolderName()) . "/admin/images/logo.png";
  551.         return Dashboard::new()
  552.             ->setTitle('<img title="Dashboard" src="' $urlImage '" />
  553.             
  554.             ')
  555.             //->renderContentMaximized()
  556.             ->setFaviconPath("../themes/" strtolower($mainSettings->getAssetFolderName()) . "/admin/images/icon.png")
  557.             ->setTranslationDomain($nameSpaceTrans)
  558.             ->disableUrlSignatures()
  559.             ->setLocales(
  560.                 [
  561.                     'fr' => 'Français',
  562.                     'en' => 'English',
  563.                     'ar' => 'Arabe',
  564.                 ]
  565.             )
  566.         ;
  567.     }
  568.     public function configureMenuItems(): iterable
  569.     {
  570.         /* START : Les Extensions IlaveU */
  571.         $applications $this->doctrine->getManager()->getRepository(Application::class)->findBy(["isEnabled" => true], ["menuOrder" => "ASC"]);
  572.         $settings $this->doctrine->getManager()->getRepository(Settings::class)->findOneBy(["code" => "main"]);
  573.         //$finder = new Finder();
  574.         $filesystem = new Filesystem();
  575.         //$finder->directories()->in(__DIR__."/../../IlaveU")->depth('== 0');
  576.         // For Principal Bundles (ShopBundle + FrontBundle + ...) 
  577.         foreach ($applications as $singleApplication) {
  578.             $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/" $singleApplication->getName() . "/IlaveU" $singleApplication->getName() . ".php");
  579.             if (!$bundleExist) {
  580.                 continue;
  581.             }
  582.             $bundleName $singleApplication->getName();
  583.             // Les themes systemes IlaveU (FrontBundle Themes)
  584.             if ($bundleName == "FrontBundle") {
  585.                 $bundleDashboardController 'App\IlaveU\FrontBundle\Themes\\' $settings->getFrontTheme() . '\Controller\DashboardController';
  586.             } else {
  587.                 $bundleDashboardController 'App\IlaveU\\' $bundleName '\Controller\DashboardController';
  588.             }
  589.             $dashboard = new $bundleDashboardController();
  590.             foreach ($dashboard->configureMenuItems() as $menu) {
  591.                 yield $menu;
  592.             }
  593.         }
  594.         // For Additional Apps Bundles (POSBundle + OtherBundle + ...)
  595.         foreach ($applications as $singleApplication) {
  596.             if ($singleApplication->getParentApplication()) {
  597.                 continue;
  598.             }
  599.             $menuArray = [];
  600.             $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/Apps/" $singleApplication->getName() . "/IlaveU" $singleApplication->getName() . ".php");
  601.             if (!$bundleExist) {
  602.                 continue;
  603.             }
  604.             $bundleName $singleApplication->getName();
  605.             $bundleDashboardController 'App\IlaveU\Apps\\' $bundleName '\Controller\DashboardController';
  606.             $dashboard = new $bundleDashboardController();
  607.             foreach ($dashboard->configureMenuItems() as $menu) {
  608.                 yield $menu;
  609.             }
  610.             //SubApplications 
  611.             foreach ($singleApplication->getSubApplications() as $subApplication) {
  612.                 $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/Apps/" $subApplication->getName() . "/IlaveU" $subApplication->getName() . ".php");
  613.                 if (!$bundleExist) {
  614.                     continue;
  615.                 }
  616.                 $bundleName $subApplication->getName();
  617.                 $bundleDashboardController 'App\IlaveU\Apps\\' $bundleName '\Controller\DashboardController';
  618.                 $dashboard = new $bundleDashboardController();
  619.                 foreach ($dashboard->configureMenuItems() as $menu) {
  620.                     yield $menu;
  621.                 }
  622.             }
  623.         }
  624.         /* END : Les Extensions IlaveU */
  625.         yield MenuItem::section('Commandes');
  626.         
  627.         yield MenuItem::linkToCrud('Commandes réglées''fas fa-check-circle'Order::class)
  628.             ->setController(PaidOrdersCrudController::class);
  629.         yield MenuItem::linkToCrud('Commandes à payer''fas fa-clock'Order::class)
  630.             ->setController(UnpaidOrdersCrudController::class);
  631.         yield MenuItem::section('Parametres');
  632.         //yield MenuItem::linkToRoute('Theme Designer', 'fas fa-shield-alt', "website_theme_grapesjs_edit")->setPermission("ROLE_ADMIN_DEV");
  633.         yield MenuItem::linkToRoute('Apps Store''fas fa-shield-alt'"app_store")->setPermission("ROLE_ADMIN_DEV");
  634.         yield MenuItem::linkToCrud('Utilisateurs''fas fa-shield-alt'User::class)->setController(UserCrudController::class);
  635.         yield MenuItem::linkToCrud('Passwords''fas fa-shield-alt'User::class)->setController(UserPasswordCrudController::class);
  636.         //yield MenuItem::linkToCrud('Roles', 'fas fa-shield-alt', Role::class);
  637.         //yield MenuItem::linkToCrud('LinkType', 'fas fa-gears', LinkType::class)->setPermission("ROLE_ADMIN_DEV");
  638.         // yield MenuItem::linkToCrud('Link', 'fas fa-gears', Link::class)->setPermission("ROLE_ADMIN_DEV");
  639.         //yield MenuItem::linkToCrud('Applications', 'fas fa-shield-alt', Application::class)->setPermission("ROLE_ADMIN_DEV");
  640.         yield MenuItem::linkToCrud('Notifications''fas fa-shield-alt'Notification::class)->setPermission("ROLE_ADMIN_DEV");
  641.         yield MenuItem::linkToRoute('Push mobile (tous)''fas fa-mobile-screen-button''admin_push_broadcast')->setPermission("ROLE_ADMIN_DEV");
  642.         yield MenuItem::linkToRoute('Text to speech''fas fa-shield-alt'"open_ai_tts")->setPermission("ROLE_ADMIN_DEV");
  643.         yield MenuItem::linkToCrud('Historique''fas fa-shield-alt'EntityLog::class)->setPermission("ROLE_ADMIN");
  644.         //yield MenuItem::linkToCrud('Settings', 'fas fa-shield-alt', Settings::class)
  645.          //   ->setAction("edit")
  646.          //   ->setEntityId($settings->getId());
  647.        // yield MenuItem::linkToCrud('Currency Exchange', 'fas fa-shield-alt', CurrencyExchangeRate::class);
  648.         //yield MenuItem::linkToCrud('Web site theme', 'fas fa-shield-alt', WebsiteTheme::class)->setPermission("ROLE_ADMIN_DEV");
  649.        // yield MenuItem::linkToCrud('Front Themes', 'fas fa-shield-alt', FrontTheme::class)->setPermission("ROLE_ADMIN_DEV");
  650.         yield MenuItem::linkToCrud('Export Excel''fas fa-shield-alt'ExportExcel::class)->setController(ExportExcelCrudController::class)->setPermission("ROLE_ADMIN_DEV");
  651.     }
  652. }