src/Controller/Admin/DashboardController.php line 97

  1. <?php
  2. namespace App\Controller\Admin;
  3. use App\Entity\Application;
  4. use App\Entity\CurrencyExchangeRate;
  5. use App\Entity\ExportExcel;
  6. use App\Entity\FrontTheme;
  7. use App\Entity\Link;
  8. use App\Entity\LinkType;
  9. use App\Entity\LogHistory;
  10. use App\Entity\Notification;
  11. use App\Entity\Role;
  12. use App\Entity\Settings;
  13. use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
  14. use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
  15. use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\Routing\Annotation\Route;
  18. use App\Entity\User;
  19. use App\Entity\WebsiteTheme;
  20. use App\IlaveU\ShopBundle\Entity\Order\Order;
  21. use App\IlaveU\ShopBundle\Entity\Product\Product;
  22. use App\IlaveU\ShopBundle\Entity\Store\Store;
  23. use App\IlaveU\ShopBundle\Entity\Vendor\Vendor;
  24. use App\IlaveU\ShopBundle\Service\IlaveUShopStatisticProvider;
  25. use App\Repository\ApplicationRepository;
  26. use App\Service\IlaveUSettingsProvider;
  27. use Doctrine\Persistence\ManagerRegistry;
  28. use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
  29. use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
  30. use Symfony\Component\Filesystem\Filesystem;
  31. use Symfony\Component\Finder\Finder;
  32. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  33. use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
  34. use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
  35. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityRepositoryInterface;
  36. use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
  37. use Symfony\Component\HttpFoundation\Request;
  38. use Symfony\Component\HttpFoundation\RequestStack;
  39. class DashboardController extends AbstractDashboardController
  40. {
  41.     public function __construct(
  42.         private readonly ManagerRegistry $doctrine,
  43.         private readonly IlaveUShopStatisticProvider $ilaveShopStatisticProvider,
  44.         private readonly RequestStack $requestStack,
  45.     ) {}
  46.     #[Route(path'/admin'name'admin_non_locale')]
  47.     public function indexNonLocale(Request $request): Response
  48.     {
  49.         return $this->redirectToRoute("admin", ["_locale" => $request->getLocale()]);
  50.     }
  51.     #[Route('/{_locale}/admin/website-theme/grapesjs_edit'name'website_theme_grapesjs_edit')]
  52.     public function website_theme_grapesjs_edit(): Response
  53.     {
  54.         return $this->render('@IlaveU/FrontBundle/Themes/' $this->container->get('twig')->getGlobals()["settings"]->get()->getAssetFolderName() . '/templates/admin/website-theme/grapesjs.html.twig');
  55.     }
  56.     #[Route(path'/{_locale}/admin/apps-store'name'app_store')]
  57.     public function appStore(Request $requestApplicationRepository $applicationRepositoryIlaveUSettingsProvider $ilaveSettingsProvider): Response
  58.     {
  59.         $appsArray = [];
  60.         foreach ($applicationRepository->findAll() as $singleApp) {
  61.             $appsArray[] = [
  62.                 "id" => $singleApp->getId(),
  63.                 "name" => $singleApp->getName(),
  64.                 "image" => $singleApp->getImage(),
  65.                 "price" => $singleApp->getPrice(),
  66.                 "pageUrl" => $singleApp->getPageUrl(),
  67.             ];
  68.         }
  69.         return $this->render("admin/app-store.html.twig", [
  70.             "installedApps" => $appsArray,
  71.             "JWT" => $ilaveSettingsProvider->createJWTForUser($this->getUser())
  72.         ]);
  73.     }
  74.     #[Route(path'/{_locale}/admin'name'admin')]
  75.     public function index(): Response
  76.     {
  77.         $url $this->requestStack->getCurrentRequest()->server->get('REQUEST_URI');
  78.         $parsedUrl parse_url($url);
  79.         parse_str($parsedUrl['query'] ?? ''$queryParams);
  80.         if (count($queryParams) == 0) {
  81.             $queryParams['store'] = null;
  82.             $queryParams['range'] = null;
  83.             $queryParams['from'] = null;
  84.             $queryParams['to'] = null;
  85.         }
  86.         $range   $queryParams['range'] ?? 'today';
  87.         $from    $queryParams['from'] ?? null;
  88.         $to      $queryParams['to'] ?? null;
  89.         $store   $queryParams['store'] ?? null;
  90.         
  91.         if(is_numeric($store)){
  92.             $store $this->doctrine->getRepository(Store::class)->findOneBy(["id" => $store]);
  93.         }
  94.         //dd($request);
  95.         // Get list of stores
  96.         $stores $this->doctrine->getRepository(Store::class)->findAll();
  97.         // Handle predefined ranges
  98.         $now = new \DateTimeImmutable();
  99.         if ($from && $to && $queryParams['range'] == '') {
  100.             $start \DateTimeImmutable::createFromFormat('Y-m-d'$from)?->setTime(00);
  101.             $end   \DateTimeImmutable::createFromFormat('Y-m-d'$to)?->setTime(235959);
  102.         } elseif ($range === 'today') {
  103.             $start $now->modify('today')->setTime(00);
  104.             $end $now->modify('today')->setTime(235959);
  105.         } elseif ($range === 'yesterday') {
  106.             $start $now->modify('yesterday')->setTime(00);
  107.             $end $now->modify('yesterday')->setTime(235959);
  108.         } elseif ($range === 'week') {
  109.             $start $now->modify('this week')->setTime(00);
  110.             $end $now->modify('next week')->setTime(00)->modify('-1 second');
  111.         } elseif ($range === 'year') {
  112.             $start $now->modify('first day of January')->setTime(00);
  113.             $end $now->modify('last day of December')->setTime(235959);
  114.         } else { // Month By Default
  115.             $start $now->modify('first day of this month')->setTime(00);
  116.             $end $now->modify('last day of this month')->setTime(235959);
  117.         }
  118.         // If custom date range provided (overrides predefined range)
  119.         
  120.         $validOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  121.             ->select('count(orderAlias.id)')
  122.             ->andWhere("orderAlias.status <> 'draft'")
  123.             ->andWhere("orderAlias.status <> 'cancelled'")
  124.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  125.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  126.             ->setParameter('start'$start)
  127.             ->setParameter('end'$end);
  128.             if($store){
  129.                 $validOrders->andWhere("orderAlias.store = :store");
  130.                 $validOrders->setParameter('store'$store);
  131.             }
  132.             $validOrders $validOrders->getQuery()->getSingleScalarResult();
  133.         $cancelledOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  134.             ->select('count(orderAlias.id)')
  135.             ->andWhere("orderAlias.status = 'cancelled' OR orderAlias.statusShipping = 'annulee'")
  136.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  137.             ->setParameter('start'$start)
  138.             ->setParameter('end'$end);
  139.             if($store){
  140.                 $cancelledOrders->andWhere("orderAlias.store = :store");
  141.                 $cancelledOrders->setParameter('store'$store);
  142.             }
  143.             $cancelledOrders $cancelledOrders->getQuery()->getSingleScalarResult();
  144.         $shippedOrders $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  145.             ->select('count(orderAlias.id)')
  146.             ->andWhere("orderAlias.statusShipping = 'shipped'")
  147.             ->andWhere("orderAlias.status <> 'draft'")
  148.             ->andWhere("orderAlias.status <> 'cancelled'")
  149.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  150.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  151.             ->setParameter('start'$start)
  152.             ->setParameter('end'$end);
  153.             if($store){
  154.                 $shippedOrders->andWhere("orderAlias.store = :store");
  155.                 $shippedOrders->setParameter('store'$store);
  156.             }
  157.             $shippedOrders $shippedOrders->getQuery()->getSingleScalarResult();
  158.         $soldProducts =   $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  159.             ->select('sum(orderItems.quantity)')
  160.             ->leftJoin("orderAlias.orderItems""orderItems")
  161.             ->andWhere("orderAlias.status <> 'draft'")
  162.             ->andWhere("orderAlias.status <> 'cancelled'")
  163.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  164.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  165.             ->setParameter('start'$start)
  166.             ->setParameter('end'$end);
  167.             if($store){
  168.                 $soldProducts->andWhere("orderAlias.store = :store");
  169.                 $soldProducts->setParameter('store'$store);
  170.             }
  171.             $soldProducts $soldProducts->getQuery()->getSingleScalarResult();
  172.         $revenuedProducts =   $this->doctrine->getRepository(Order::class)->createQueryBuilder('orderAlias')
  173.             ->select('sum(orderItems.quantity * orderItems.price)')
  174.             ->leftJoin("orderAlias.orderItems""orderItems")
  175.             ->andWhere("orderAlias.status <> 'draft'")
  176.             ->andWhere("orderAlias.status <> 'cancelled'")
  177.             ->andWhere("orderAlias.statusShipping <> 'annulee'")
  178.             ->andWhere('orderAlias.createdAt BETWEEN :start AND :end')
  179.             ->setParameter('start'$start)
  180.             ->setParameter('end'$end);
  181.             if($store){
  182.                 $revenuedProducts->andWhere("orderAlias.store = :store");
  183.                 $revenuedProducts->setParameter('store'$store);
  184.             }
  185.             $revenuedProducts $revenuedProducts->getQuery()->getSingleScalarResult();
  186.         $averageOrderAmount $validOrders $revenuedProducts $validOrders 0;
  187.         //$inTypes = Order::IN_TYPES;
  188.         //$outTypes = Order::OUT_TYPES;
  189.         // $productInStock = $this->doctrine->createQueryBuilder()
  190.         //     ->select('p.id AS product_id, p.name AS product_name,p.initialStock AS initial_stock')
  191.         //     ->addSelect('
  192.         //         SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) AS stock_in,
  193.         //         SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END) AS stock_out,
  194.         //         (
  195.         //             p.initialStock +
  196.         //             SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) -
  197.         //             SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END)
  198.         //         ) AS current_stock
  199.         //     ')
  200.         //     ->from(Product::class, 'p')
  201.         //     ->leftJoin('p.orderItems', 'oi')
  202.         //     ->leftJoin('oi.parentOrder', 'o')
  203.         //     ->groupBy('p.id')
  204.         //     ->having('
  205.         //         (
  206.         //             p.initialStock +
  207.         //             SUM(CASE WHEN o.type IN (:inTypes) THEN oi.quantity ELSE 0 END) -
  208.         //             SUM(CASE WHEN o.type IN (:outTypes) THEN oi.quantity ELSE 0 END)
  209.         //         ) > 0
  210.         //     ')
  211.         //     ->andWhere("o.status = '".Order::STATUS_VALIDATED."'")
  212.         //     //->andWhere('o.createdAt BETWEEN :start AND :end')
  213.         //     // ->setParameter('start', $start)
  214.         //     // ->setParameter('end', $end)
  215.         //     ->setMaxResults(16)
  216.         //     ->orderBy('current_stock', 'DESC')
  217.         //     ->getQuery()
  218.         //     ->getArrayResult();
  219.         $mostOrderedCategory $this->doctrine->getRepository(Order::class)
  220.             ->createQueryBuilder('o')
  221.             ->select('c.id, c.name as categoryName, COUNT(DISTINCT o.id) as orderCount')
  222.             ->join('o.orderItems''oi')
  223.             ->join('oi.product''p')
  224.             ->join('p.categoriesProduct''c'// Assuming products have a many-to-many relationship with categories
  225.             ->where("o.status NOT IN ('draft', 'cancelled') AND o.statusShipping <> 'annulee'")
  226.             ->andWhere('o.createdAt BETWEEN :start AND :end')
  227.             ->groupBy('c.id, c.name')
  228.             ->orderBy('orderCount''DESC')
  229.             ->setMaxResults(1// Get only the top category
  230.             ->setParameter('start'$start)
  231.             ->setParameter('end'$end);
  232.             if($store){
  233.                 $mostOrderedCategory->andWhere("o.store = :store");
  234.                 $mostOrderedCategory->setParameter('store'$store);
  235.             }
  236.             $mostOrderedCategory->getQuery()->getOneOrNullResult();
  237.             $mostOrderedCategory $mostOrderedCategory->getQuery()->getOneOrNullResult();
  238.                     
  239.         $mostOrderedCategoryName $mostOrderedCategory $mostOrderedCategory['categoryName'] : 'N/A';
  240.         // Get the most ordered product
  241.         $mostOrderedProduct $this->doctrine->getRepository(Order::class)
  242.             ->createQueryBuilder('o')
  243.             ->select('p.id, p.name as productName, SUM(oi.quantity) as totalQuantity, COUNT(DISTINCT o.id) as orderCount')
  244.             ->join('o.orderItems''oi')
  245.             ->join('oi.product''p')
  246.             ->where("o.status NOT IN ('draft', 'cancelled') AND o.statusShipping <> 'annulee'")
  247.             ->andWhere('o.createdAt BETWEEN :start AND :end')
  248.             ->groupBy('p.id, p.name')
  249.             ->orderBy('totalQuantity''DESC')
  250.             ->setMaxResults(1)
  251.             ->setParameter('start'$start)
  252.             ->setParameter('end'$end);
  253.             if($store){
  254.                 $mostOrderedProduct->andWhere("o.store = :store");
  255.                 $mostOrderedProduct->setParameter('store'$store);
  256.             }
  257.             $mostOrderedProduct $mostOrderedProduct->getQuery()->getOneOrNullResult();
  258.             
  259.         $mostOrderedProductName $mostOrderedProduct $mostOrderedProduct['productName'] : 'N/A';
  260.         $stats = [
  261.             [
  262.                 'label' => 'Chiffre d\'affaires',
  263.                 'value' => (float)round($revenuedProducts2),
  264.                 'icon' => 'fa-coins',
  265.                 'cssClass' => 'stat-revenue',
  266.                 'description' => 'Revenu total généré par les ventes',
  267.                 'url' => null,
  268.                 'unit' => 'MAD',
  269.             ],
  270.             [
  271.                 'label' => 'Ventes',
  272.                 'value' => (int)$validOrders,
  273.                 'icon' => 'fa-calendar-check',
  274.                 'cssClass' => 'stat-monthly-orders',
  275.                 'description' => 'Nombre de ventes validées',
  276.                 'url' => null,
  277.                 'unit' => null,
  278.             ],
  279.             [
  280.                 'label' => 'Produits vendus',
  281.                 'value' => (int)$soldProducts,
  282.                 'icon' => 'fa-boxes',
  283.                 'cssClass' => 'stat-products-sold',
  284.                 'description' => 'Nombre total de produits vendus',
  285.                 'url' => null,
  286.                 'unit' => null,
  287.             ],
  288.             [
  289.                 'label' => 'Panier moyen',
  290.                 'value' => (float)round($averageOrderAmount2),
  291.                 'icon' => 'fa-shopping-basket',
  292.                 'cssClass' => 'stat-average-basket',
  293.                 'description' => 'Montant moyen dépensé par commande',
  294.                 'url' => null,
  295.                 'unit' => 'MAD',
  296.             ],
  297.             [
  298.                 'label' => 'Commandes annulées',
  299.                 'value' => (float)$cancelledOrders,
  300.                 'icon' => 'fa-undo',
  301.                 'cssClass' => 'stat-refunds',
  302.                 'description' => 'Nombre de commandes annulées',
  303.                 'url' => null,
  304.                 'unit' => null,
  305.             ],
  306.             [
  307.                 'label' => 'Commandes livrées',
  308.                 'value' => (float)$shippedOrders,
  309.                 'icon' => 'fa-truck',
  310.                 'cssClass' => 'stat-delivered-orders',
  311.                 'description' => 'Nombre de commandes livrées',
  312.                 'url' => null,
  313.                 'unit' => null,
  314.             ],
  315.             [
  316.                 'label' => 'Catégorie phare',
  317.                 'value' => $mostOrderedCategoryName,
  318.                 'icon' => 'fa-star',
  319.                 'cssClass' => 'stat-top-category',
  320.                 'description' => 'Catégorie la plus vendue ce mois-ci',
  321.                 'url' => null,
  322.                 'unit' => null,
  323.             ],
  324.             [
  325.                 'label' => 'Produit phare',
  326.                 'value' => $mostOrderedProductName,
  327.                 'icon' => 'fa-star',
  328.                 'cssClass' => 'stat-top-product',
  329.                 'description' => 'Produit la plus vendu ce mois-ci',
  330.                 'url' => null,
  331.                 'unit' => null,
  332.             ],
  333.         ];
  334.         $productLabels = [];
  335.         $productChartData = [];
  336.         // foreach ($productInStock as $product) {
  337.         //     $productLabels[] = $product['product_name'];
  338.         //     $productChartData[] = (float) $product['current_stock']; // cast to float for Chart.js
  339.         // }
  340.         // $productChartData = [
  341.         //     'labels' => $productLabels,
  342.         //     'data' => $productChartData,
  343.         // ];
  344.         return $this->render(
  345.             "bundles/EasyAdminBundle/welcome.html.twig",
  346.             [
  347.                 'stores' => $stores,
  348.                 'stats' => $stats,
  349.                 'productChartData' => $productChartData,
  350.                 'queryParams' => $queryParams,
  351.             ]
  352.         );
  353.     }
  354.     public function configureCrud(): Crud
  355.     {
  356.         return
  357.             Crud::new()
  358.             ->setDefaultSort(["id" => 'DESC'])
  359.             ->setPaginatorPageSize(12)
  360.         ;
  361.     }
  362.     public function configureDashboard(): Dashboard
  363.     {
  364.         $mainSettings $this->doctrine->getManager()->getRepository(Settings::class)->findOneBy(["code" => "main"]);
  365.         $nameSpaceTrans strtolower($mainSettings->getProjectName()) . "-admin";
  366.         $urlImage "../themes/" strtolower($mainSettings->getAssetFolderName()) . "/admin/images/logo.png";
  367.         return Dashboard::new()
  368.             ->setTitle('<img title="Dashboard" src="' $urlImage '" />
  369.             
  370.             ')
  371.             //->renderContentMaximized()
  372.             ->setFaviconPath("../themes/" strtolower($mainSettings->getAssetFolderName()) . "/admin/images/icon.png")
  373.             ->setTranslationDomain($nameSpaceTrans)
  374.             ->disableUrlSignatures()
  375.             ->setLocales(
  376.                 [
  377.                     'fr' => 'Français',
  378.                     'en' => 'English',
  379.                     'ar' => 'Arabe',
  380.                 ]
  381.             )
  382.         ;
  383.     }
  384.     public function configureMenuItems(): iterable
  385.     {
  386.         /* START : Les Extensions IlaveU */
  387.         $applications $this->doctrine->getManager()->getRepository(Application::class)->findBy(["isEnabled" => true], ["menuOrder" => "ASC"]);
  388.         $settings $this->doctrine->getManager()->getRepository(Settings::class)->findOneBy(["code" => "main"]);
  389.         //$finder = new Finder();
  390.         $filesystem = new Filesystem();
  391.         //$finder->directories()->in(__DIR__."/../../IlaveU")->depth('== 0');
  392.         // For Principal Bundles (ShopBundle + FrontBundle + ...) 
  393.         foreach ($applications as $singleApplication) {
  394.             $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/" $singleApplication->getName() . "/IlaveU" $singleApplication->getName() . ".php");
  395.             if (!$bundleExist) {
  396.                 continue;
  397.             }
  398.             $bundleName $singleApplication->getName();
  399.             // Les themes systemes IlaveU (FrontBundle Themes)
  400.             if ($bundleName == "FrontBundle") {
  401.                 $bundleDashboardController 'App\IlaveU\FrontBundle\Themes\\' $settings->getFrontTheme() . '\Controller\DashboardController';
  402.             } else {
  403.                 $bundleDashboardController 'App\IlaveU\\' $bundleName '\Controller\DashboardController';
  404.             }
  405.             $dashboard = new $bundleDashboardController();
  406.             foreach ($dashboard->configureMenuItems() as $menu) {
  407.                 yield $menu;
  408.             }
  409.         }
  410.         // For Additional Apps Bundles (POSBundle + OtherBundle + ...)
  411.         foreach ($applications as $singleApplication) {
  412.             if ($singleApplication->getParentApplication()) {
  413.                 continue;
  414.             }
  415.             $menuArray = [];
  416.             $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/Apps/" $singleApplication->getName() . "/IlaveU" $singleApplication->getName() . ".php");
  417.             if (!$bundleExist) {
  418.                 continue;
  419.             }
  420.             $bundleName $singleApplication->getName();
  421.             $bundleDashboardController 'App\IlaveU\Apps\\' $bundleName '\Controller\DashboardController';
  422.             $dashboard = new $bundleDashboardController();
  423.             foreach ($dashboard->configureMenuItems() as $menu) {
  424.                 yield $menu;
  425.             }
  426.             //SubApplications 
  427.             foreach ($singleApplication->getSubApplications() as $subApplication) {
  428.                 $bundleExist $filesystem->exists(__DIR__ "/../../IlaveU/Apps/" $subApplication->getName() . "/IlaveU" $subApplication->getName() . ".php");
  429.                 if (!$bundleExist) {
  430.                     continue;
  431.                 }
  432.                 $bundleName $subApplication->getName();
  433.                 $bundleDashboardController 'App\IlaveU\Apps\\' $bundleName '\Controller\DashboardController';
  434.                 $dashboard = new $bundleDashboardController();
  435.                 foreach ($dashboard->configureMenuItems() as $menu) {
  436.                     yield $menu;
  437.                 }
  438.             }
  439.         }
  440.         /* END : Les Extensions IlaveU */
  441.         yield MenuItem::section('Parametres');
  442.         yield MenuItem::linkToRoute('Theme Designer''fas fa-shield-alt'"website_theme_grapesjs_edit")->setPermission("ROLE_ADMIN_DEV");
  443.         yield MenuItem::linkToRoute('Apps Store''fas fa-shield-alt'"app_store")->setPermission("ROLE_ADMIN_DEV");
  444.         yield MenuItem::linkToCrud('Utilisateurs''fas fa-shield-alt'User::class)->setController(UserCrudController::class);
  445.         yield MenuItem::linkToCrud('Passwords''fas fa-shield-alt'User::class)->setController(UserPasswordCrudController::class);
  446.         yield MenuItem::linkToCrud('Roles''fas fa-shield-alt'Role::class);
  447.         yield MenuItem::linkToCrud('LinkType''fas fa-gears'LinkType::class)->setPermission("ROLE_ADMIN_DEV");
  448.         yield MenuItem::linkToCrud('Link''fas fa-gears'Link::class)->setPermission("ROLE_ADMIN_DEV");
  449.         yield MenuItem::linkToCrud('Applications''fas fa-shield-alt'Application::class)->setPermission("ROLE_ADMIN_DEV");
  450.         yield MenuItem::linkToCrud('Notifications''fas fa-shield-alt'Notification::class)->setPermission("ROLE_ADMIN_DEV");
  451.         yield MenuItem::linkToRoute('Text to speech''fas fa-shield-alt'"open_ai_tts")->setPermission("ROLE_ADMIN_DEV");
  452.         yield MenuItem::linkToCrud('LogHistory''fas fa-shield-alt'LogHistory::class)->setPermission("ROLE_ADMIN_DEV");
  453.         yield MenuItem::linkToCrud('Settings''fas fa-shield-alt'Settings::class)
  454.             ->setAction("edit")
  455.             ->setEntityId($settings->getId());
  456.         yield MenuItem::linkToCrud('Currency Exchange''fas fa-shield-alt'CurrencyExchangeRate::class);
  457.         yield MenuItem::linkToCrud('Web site theme''fas fa-shield-alt'WebsiteTheme::class)->setPermission("ROLE_ADMIN_DEV");
  458.         yield MenuItem::linkToCrud('Front Themes''fas fa-shield-alt'FrontTheme::class)->setPermission("ROLE_ADMIN_DEV");
  459.         yield MenuItem::linkToCrud('Export Excel''fas fa-shield-alt'ExportExcel::class)->setController(ExportExcelCrudController::class)->setPermission("ROLE_ADMIN_DEV");
  460.     }
  461. }