src/Controller/Admin/Dashboard/DashboardController.php line 1458

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Admin\Dashboard;
  3. use App\Dto\SellerDetailRequestDto;
  4. use App\Entity\Product;
  5. use App\Enum\ProductTypeEnum;
  6. use App\Entity\ProductsSold;
  7. use App\Entity\Sales;
  8. use App\Entity\StockInfo;
  9. use App\Entity\User;
  10. use App\Entity\Warehouse;
  11. use App\Repository\ProductRepository;
  12. use App\Repository\StockRepository;
  13. use App\Repository\ProductMonthlyStatsRepository;
  14. use App\Service\Analysis\FinancialForecastingService;
  15. use App\Services\Product\Impl\ProductServiceImpl;
  16. use App\Services\Sales\Impl\SalesProductsServiceImpl;
  17. use App\Services\Sales\Impl\SalesServiceImpl;
  18. use App\Services\Sales\SalesService;
  19. use App\Services\Stock\StockService;
  20. use App\Services\Stock\StockTransferService;
  21. use App\Services\User\UserService;
  22. use App\Services\Warehouse\Impl\WarehouseServiceImpl;
  23. use App\Utils\CurrencyHelper;
  24. use App\Utils\DateUtil;
  25. use App\Utils\Enums\DateZoneEnums;
  26. use App\Utils\Referer;
  27. use DateInterval;
  28. use DatePeriod;
  29. use Doctrine\Common\Collections\ArrayCollection;
  30. use Doctrine\ORM\EntityManagerInterface;
  31. use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;
  32. use Knp\Component\Pager\PaginatorInterface;
  33. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  34. use Symfony\Component\HttpFoundation\JsonResponse;
  35. use Symfony\Component\HttpFoundation\Request;
  36. use Symfony\Component\HttpFoundation\Response;
  37. use Symfony\Component\Routing\Annotation\Route;
  38. use Symfony\Component\Routing\RouterInterface;
  39. use Symfony\Component\Validator\Constraints\DateTime;
  40. use App\Repository\SalesRepository;
  41. use function Doctrine\ORM\QueryBuilder;
  42. class DashboardController extends AbstractController
  43. {
  44.     use Referer;
  45.     private UserService $userService;
  46.     private StockService $stockService;
  47.     private SalesService $salesService;
  48.     private StockTransferService $stockTransferService;
  49.     private EntityManagerInterface $entityManager;
  50.     private StockRepository $stockRepository;
  51.     private SalesServiceImpl $salesServiceImpl;
  52.     private ProductServiceImpl $productServiceImpl;
  53.     private PaginatorInterface $paginator;
  54.     private WarehouseServiceImpl $warehouseService;
  55.     private SalesProductsServiceImpl $salesProductsServiceImpl;
  56.     /**
  57.      * @param UserService $userService
  58.      * @param StockService $stockService
  59.      * @param SalesService $salesService
  60.      * @param StockTransferService $stockTransferService
  61.      * @param EntityManagerInterface $entityManager
  62.      * @param StockRepository $stockRepository
  63.      * @param SalesServiceImpl $salesServiceImpl
  64.      * @param ProductServiceImpl $productServiceImpl
  65.      * @param PaginatorInterface $paginator
  66.      * @param WarehouseServiceImpl $warehouseService
  67.      * @param SalesProductsServiceImpl $salesProductsServiceImpl
  68.      */
  69.     public function __construct(UserService $userServiceStockService $stockServiceSalesService $salesServiceStockTransferService $stockTransferServiceEntityManagerInterface $entityManagerStockRepository $stockRepositorySalesServiceImpl $salesServiceImplProductServiceImpl $productServiceImplPaginatorInterface $paginatorWarehouseServiceImpl $warehouseServiceSalesProductsServiceImpl $salesProductsServiceImpl)
  70.     {
  71.         $this->userService $userService;
  72.         $this->stockService $stockService;
  73.         $this->salesService $salesService;
  74.         $this->stockTransferService $stockTransferService;
  75.         $this->entityManager $entityManager;
  76.         $this->stockRepository $stockRepository;
  77.         $this->salesServiceImpl $salesServiceImpl;
  78.         $this->productServiceImpl $productServiceImpl;
  79.         $this->paginator $paginator;
  80.         $this->warehouseService $warehouseService;
  81.         $this->salesProductsServiceImpl $salesProductsServiceImpl;
  82.     }
  83.     public function getProfitByPeriots(DateZoneEnums $dateZoneEnums$year null)
  84.     {
  85.         if ($year == null) {
  86.             $year date('Y');
  87.         }
  88.         $salesRepo $this->entityManager->getRepository(Sales::class);
  89.         if ($dateZoneEnums->value == 'monthly') {
  90.             $time = new \DateTimeImmutable('1 month ago');
  91.             $endTime = new \DateTimeImmutable('now');
  92.         }
  93.         if ($dateZoneEnums->value == 'weekly') {
  94.             $time = new \DateTimeImmutable('1 week ago');
  95.             $endTime = new \DateTimeImmutable('now');
  96.         }
  97.         if ($dateZoneEnums->value == 'yearly') {
  98.             $time = new \DateTimeImmutable('first day of January ' $year);
  99.             $endTime = new \DateTimeImmutable('last day of December ' $year ' 23:59:59');
  100.         }
  101.         $qb $salesRepo->createQueryBuilder('s')
  102.             ->leftJoin('s.productsSolds''productsSolds')
  103.             ->where('s.salesDate >= :time')
  104.             ->andWhere('s.salesDate <= :endTime')
  105.             ->setParameter('time'$time)
  106.             ->setParameter('endTime'$endTime);
  107.         if (!$this->canCurrentUserSeeHiddenSales()) {
  108.             $qb->andWhere('s.visible = :visible')
  109.                 ->setParameter('visible'true);
  110.         }
  111.         $sales $qb->getQuery()->getResult();
  112.         $totalProfit 0.00;
  113.         foreach ($sales as $sale) {
  114.             foreach ($sale->getProductsSolds() as $productsSold) {
  115.                 $totalProfit += $this->salesProductsServiceImpl->calculateProfitByProductSold($productsSold);
  116.             }
  117.         }
  118.         return CurrencyHelper::convertToCurrency($totalProfit);
  119.     }
  120.     #[Route('/get-products-summary'name'get_products_summary')]
  121.     public function getProductsSummary(Request $requestProductRepository $productRepository)
  122.     {
  123.         $s = isset($request->get('search')['value']) ? $request->get('search')['value'] : null;
  124.         $startDate $request->get('startDate') != null $request->get('startDate') : (new \DateTimeImmutable('now'))->modify('-1 month')->format('Y-m-d');
  125.         $endDate $request->get('endDate') != null $request->get('endDate') : (new \DateTimeImmutable('now'))->format('Y-m-d');
  126.         $id $request->get('id');
  127.         $column $request->get('order') != null $request->get('order')[0]['column'] : '0';
  128.         $seller $request->get('sellerId') == null $request->get('sellerId');
  129.         $dir $request->get('order') != null $request->get('order')[0]['dir'] : 'asc';
  130.         $limit $request->get('length');
  131.         if ($request->get('start'))
  132.             $page + ($request->get('start') / $limit);
  133.         else
  134.             $page 1;
  135.         $startDate \DateTimeImmutable::createFromFormat('Y-m-d'$startDate);
  136.         $endDate \DateTimeImmutable::createFromFormat('Y-m-d'$endDate);
  137.         $query $this->entityManager->getRepository(Product::class)->createQueryBuilder('p')
  138.             ->leftJoin('p.productsSolds''productsSolds')
  139.             ->leftJoin('productsSolds.sales''sales')
  140.             ->leftJoin('sales.seller''seller')
  141.             ->leftJoin('p.measurement''w')
  142.             ->where('p.salePrice LIKE :s OR p.warehousePrice LIKE :s OR p.purchasePrice LIKE :s OR p.name LIKE :s OR p.description LIKE :s OR p.code LIKE :s')
  143.             ->setParameter('s'"%$s%")
  144.             ->orWhere('w.measurement LIKE :s')
  145.             ->setParameter('s'"%$s%");
  146.         if ($seller != null) {
  147.             $query->andWhere('seller.id = :sellerId')
  148.                 ->setParameter('sellerId'$seller);
  149.         }
  150.         $query $query->getQuery();
  151.         $products $query->getResult();
  152.         //        switch ($column){
  153. //            case 0:
  154. //                $column = 'id';
  155. //                break;
  156. //            case 1:
  157. //                $column = 'productName';
  158. //                break;
  159. //            case 2:
  160. //                $column = 'code';
  161. //                break;
  162. //            case 3:
  163. //                $column = 'soldPrice';
  164. //                break;
  165. //            case 4:
  166. //                $column = 'totalEarn';
  167. //                break;
  168. //            case 5:
  169. //                $column = 'totalProfit';
  170. //                break;
  171. //        }
  172.         $datas $this->createArrayForProductsDatatable(
  173.             $products,
  174.             $startDate,
  175.             $endDate,
  176.             $column,
  177.             $dir,
  178.             $page,
  179.             $limit
  180.         );
  181.         //        dd($datas);
  182. //
  183. //        dd($column);
  184.         return $this->json($datas200);
  185.         dd($products->getItems());
  186.         return $this->json(['data' => 'ss'], 200);
  187.         $products $this->entityManager->getRepository(Product::class);
  188.     }
  189.     public function createArrayForProductsDatatable($products\DateTimeImmutable $startDate\DateTimeImmutable $endDate$column$dir$page$limit)
  190.     {
  191.         $records = [];
  192.         $records["data"] = [];
  193.         /**
  194.          * @var Product $entity
  195.          */
  196.         foreach ($products as $entity) {
  197.             $records["data"][] = array(
  198.                 $entity->getId(),
  199.                 $entity->getName(),
  200.                 $entity->getCode() . " - " . ($entity->getMeasurement() != null $entity->getMeasurement()->getMeasurement() : ''),
  201.                 $this->salesServiceImpl->getTotalSoldQuantityByDateSellerAndProduct($startDate$endDatenull$entity->getId()),
  202.                 $this->salesServiceImpl->getTotalSalesPriceByDateSellerAndProduct($startDate$endDatenull$entity->getId()),
  203.                 $this->salesServiceImpl->getTotalProfitPriceByDateSellerAndProduct($startDate$endDatenull$entity->getId()),
  204.                 "<button class='btn btn-primary product-details-btn' data-enddate='" $endDate->format('Y-m-d') . "' data-startdate='" $startDate->format('Y-m-d') . "' data-pid='" $entity->getId() . "'>Detay</button>"
  205.             );
  206.         }
  207.         $this->sortArrayByKey($records["data"], $columnfalse$dir == 'asc');
  208.         $pagination $this->paginator->paginate(
  209.             $records["data"],
  210.             $page,
  211.             $limit
  212.         );
  213.         $records['recordsTotal'] = $pagination->getTotalItemCount();
  214.         $records['recordsFiltered'] = $pagination->getTotalItemCount();
  215.         $records["data"] = $pagination->getItems();
  216.         return $records;
  217.     }
  218.     #[Route('/sales-prices-and-profits-by-year-range-month'name'sales_prices_and_profits_by_year_range_month')]
  219.     public function getSalesPricesAndProfitsByYearRangeMonth(Request $request)
  220.     {
  221.         $year $request->get('year') != null $request->get('year') : date('Y');
  222.         $months = [];
  223.         $format 'Y-m-d';
  224.         for ($month 1$month <= 12$month++) {
  225.             //            if($month > date('m'))
  226. //                break;
  227.             $startOfMonth \DateTimeImmutable::createFromFormat($format"$year-$month-01");
  228.             $endOfMonth = (\DateTimeImmutable::createFromFormat($format"$year-$month-01"))->modify('last day of this month');
  229.             $months[$month 1]['start'] = $startOfMonth;
  230.             $months[$month 1]['end'] = $endOfMonth;
  231.         }
  232.         $data = [];
  233.         foreach ($months as $month) {
  234.             $data['profit'][] = $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate($month['start'], $month['end']);
  235.             $data['sales'][] = $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate($month['start'], $month['end']);
  236.         }
  237.         //        $qb = $this->entityManager->getRepository(Sales::class)->createQueryBuilder('s')
  238. //                    ->select('MONTH(s.salesDate) as month, SUM(s.totalPurchasePrice) as total')
  239. //                    ->where('YEAR(s.salesDate) = :year')
  240. //                    ->setParameter('year', $year)
  241. //                    ->groupBy('month')
  242. //                    ->orderBy('month', 'ASC')
  243. //                    ->getQuery();
  244.         //        dd($qb->getResult());
  245.         return $this->json($data200);
  246.     }
  247.     function sortArrayByKey(&$array$key$string false$asc true)
  248.     {
  249.         if ($string) {
  250.             usort($array, function ($a$b) use (&$key, &$asc) {
  251.                 if ($asc)
  252.                     return strcmp(strtolower($a[$key]), strtolower($b[$key]));
  253.                 else
  254.                     return strcmp(strtolower($b[$key]), strtolower($a[$key]));
  255.             });
  256.         } else {
  257.             usort($array, function ($a$b) use (&$key, &$asc) {
  258.                 if ($a[$key] == $b[$key]) {
  259.                     return 0;
  260.                 }
  261.                 if ($asc)
  262.                     return ($a[$key] < $b[$key]) ? -1;
  263.                 else
  264.                     return ($a[$key] > $b[$key]) ? -1;
  265.             });
  266.         }
  267.     }
  268.     #[Route('/admin'name'app_panel_dashboard')]
  269.     public function index(Request $requestSellerDetailRequestDto $sellerDetailRequestDto\App\Repository\ProductMonthlyStatsRepository $statsRepositoryFinancialForecastingService $financialService\App\Service\Analysis\StrategicForecastingService $forecastingService): Response
  270.     {
  271.         if ($request->get('year') != null) {
  272.             $date = new \DateTimeImmutable('first day of January ' $request->get('year'));
  273.             $year $request->get('year');
  274.         } else {
  275.             $date = new \DateTimeImmutable('first day of January this year');
  276.             $year date('Y');
  277.         }
  278.         $mountly $this->salesService->getEarningsByPeriot(DateZoneEnums::MONTHLY);
  279.         $weekly $this->salesService->getEarningsByPeriot(DateZoneEnums::WEEKLY);
  280.         $yearly $this->salesService->getEarningsByPeriot(DateZoneEnums::YEARLY);
  281.         $profitMothly $this->getProfitByPeriots(DateZoneEnums::MONTHLY$year);
  282.         $profitWeekly $this->getProfitByPeriots(DateZoneEnums::WEEKLY$year);
  283.         $months = ['January''February''March''April''May''June''July''August''September''October''November''December'];
  284.         $monthsTr = ['Ocak''Şubat''Mart''Nisan''Mayıs''Haziran''Temmuz''Ağustos''Eylül''Ekim''Kasım''Aralık'];
  285.         $array = [];
  286.         foreach ($months as $key => $month) {
  287.             $thisMonth = (int) date('m');
  288.             if ($month != $months[$thisMonth 1]) {
  289.                 $startDate = new \DateTimeImmutable('first day of ' $months[$key] . ' ' $year);
  290.                 $endDate = new \DateTimeImmutable('last day of ' $months[$key] . ' ' $year);
  291.                 $earn $this->salesService->getEarningsBetwenTwoDate($startDate$endDate);
  292.                 $array[] = [
  293.                     $monthsTr[$key],
  294.                     $earn $earn 0.00,
  295.                 ];
  296.             } else {
  297.                 $startDate = new \DateTimeImmutable('first day of ' $months[$thisMonth 1] . ' ' $year);
  298.                 $endDate = new \DateTimeImmutable('now');
  299.                 $array[] = [
  300.                     $monthsTr[$thisMonth 1],
  301.                     $this->salesService->getEarningsBetwenTwoDate($startDate$endDate),
  302.                 ];
  303.                 break;
  304.             }
  305.         }
  306.         $inTransitTransferNumber $this->stockTransferService->getNumberOfInTransitTransfers();
  307.         $allSallers $this->entityManager->getRepository(User::class)->findAll();
  308.         $allWarehouses $this->entityManager->getRepository(Warehouse::class)->findAll();
  309.         $allProducts $this->entityManager->getRepository(Product::class)->findAll();
  310.         $uniqueProductCodes = [];
  311.         foreach ($allProducts as $p) {
  312.             $c $p->getCode();
  313.             if ($c) {
  314.                 $uniqueProductCodes[] = $c;
  315.             }
  316.         }
  317.         $uniqueProductCodes array_unique($uniqueProductCodes);
  318.         sort($uniqueProductCodes);
  319.         $requestedProducts $request->get('pproductId');
  320.         $products = [];
  321.         if (is_array($requestedProducts)) {
  322.             $products array_values(array_filter($requestedProducts));
  323.         } elseif (!empty($requestedProducts)) {
  324.             $products = [(int) $requestedProducts];
  325.         }
  326.         if (empty($products)) {
  327.             $ps $this->entityManager->getRepository(Product::class)
  328.                 ->createQueryBuilder('c')
  329.                 ->select('c.id')
  330.                 ->setMaxResults(5)
  331.                 ->getQuery()
  332.                 ->getResult();
  333.             foreach ($ps as $p) {
  334.                 $products[] = (int) $p['id'];
  335.             }
  336.         }
  337.         $startDate $request->get('pstartDate') ? new \DateTimeImmutable($request->get('pstartDate')) : new \DateTimeImmutable("30 days ago");
  338.         $finishDate $request->get('pfinishDate') ? new \DateTimeImmutable($request->get('pfinishDate')) : new \DateTimeImmutable('now');
  339.         if (isset($this->entityManager->getRepository(Warehouse::class)->createQueryBuilder('c')->leftJoin('c.sales''sales')->where('sales.id is not null')->setMaxResults(1)->getQuery()->getResult()[0])) {
  340.             $warehouseId $request->get('pwarehouseId') ? $request->get('pwarehouseId') : $this->entityManager->getRepository(Warehouse::class)->createQueryBuilder('c')->leftJoin('c.sales''sales')->where('sales.id is not null')->setMaxResults(1)->getQuery()->getResult()[0]->getId();
  341.         } else {
  342.             try {
  343.                 $warehouseId $request->get('pwarehouseId') ? $request->get('pwarehouseId') : $this->entityManager->getRepository(Warehouse::class)->createQueryBuilder('c')->leftJoin('c.sales''sales')->setMaxResults(1)->getQuery()->getResult()[0]->getId();
  344.             } catch (\Exception $e) {
  345.                 $warehouseId 0;
  346.             }
  347.         }
  348.         $sellerId $request->get('psellerId');
  349.         $periyot 0;
  350.         if ($request->get('periyot') !== null) {
  351.             if ($request->get('periyot') == 'weekly') {
  352.                 $periyot 7;
  353.                 $periyotAdi 'Hafta';
  354.             }
  355.             if ($request->get('periyot') == 'mothly') {
  356.                 $periyot 30;
  357.                 $periyotAdi 'Ay';
  358.             }
  359.             if ($request->get('periyot') == 'daily') {
  360.                 $periyot 1;
  361.                 $periyotAdi 'Gün';
  362.             }
  363.         } else {
  364.             $periyot 7;
  365.             $periyotAdi 'Hafta';
  366.         }
  367.         $fark $startDate->diff($finishDate);
  368.         if ($fark->!= 0) {
  369.             $fark $fark->30 $fark->d;
  370.         } else {
  371.             $fark $fark->d;
  372.         }
  373.         $periyotSayisi = (int) ceil($fark $periyot);
  374.         $arrays = [];
  375.         $baslangicTarihi $startDate->format('Y-m-d');
  376.         $bitisTarihi $startDate->modify('+' $periyot 'days')->format('Y-m-d');
  377.         for ($i 0$i $periyotSayisi$i++) {
  378.             if ($finishDate $bitisTarihi) {
  379.                 $bitisTarihi $finishDate;
  380.             }
  381.             foreach ($products as $productId) {
  382.                 $product $this->entityManager->getRepository(Product::class)->find($productId);
  383.                 if (!$product) {
  384.                     continue;
  385.                 }
  386.                 $periotData $this->getProductsByPeriot($baslangicTarihi$bitisTarihi$productId$warehouseId$sellerId);
  387.                 $totalAmount 0.00;
  388.                 if (!empty($periotData) && isset($periotData[0]['totalAmount'])) {
  389.                     $totalAmount = (float) $periotData[0]['totalAmount'];
  390.                 }
  391.                 $arrays[$i '.' $periyotAdi][$product->getName()] = $totalAmount;
  392.             }
  393.             $baslangicTarihi $bitisTarihi;
  394.             $yeniBaglangicTarihi = new \DateTimeImmutable($baslangicTarihi);
  395.             $bitisTarihi $yeniBaglangicTarihi->modify('+' $periyot 'days')->format('Y-m-d');
  396.         }
  397.         $sellers $this->entityManager->getRepository(User::class)->findAll();
  398.         $urun = [];
  399.         $periyotlar = [];
  400.         foreach ($arrays as $key => $arrayy) {
  401.             $periyotlar[] = $key;
  402.             foreach ($arrayy as $key => $arr) {
  403.                 $urun[$key][] = $arr;
  404.             }
  405.         }
  406.         $lowStockProducts $this->getLowStockProducts();
  407.         $sellersDetail $this->getSalesBySeller(
  408.             $sellerDetailRequestDto->getSellerStartDate(),
  409.             $sellerDetailRequestDto->getSellerFinishDate(),
  410.             $sellerDetailRequestDto->getSellerProducts(),
  411.             $sellerDetailRequestDto->getSellerId(),
  412.             $sellerDetailRequestDto->getSelectWarehouseForSellersTable(),
  413.         );
  414.         // --- NEW: 2025 Strategic Data Calculation ---
  415.         $stats2025 $statsRepository->findBy(['year' => 2025]);
  416.         $totalRevenue2025 0;
  417.         $totalProfit2025 0;
  418.         foreach ($stats2025 as $stat) {
  419.             $totalRevenue2025 += $stat->getTotalSalesQty() * $stat->getAvgSalesPrice();
  420.             // Net Profit field might be string or float, ensure float
  421.             $cost = (float) $stat->getAvgCostPrice();
  422.             $price = (float) $stat->getAvgSalesPrice();
  423.             $qty = (float) $stat->getTotalSalesQty();
  424.             $totalProfit2025 += ($price $cost) * $qty;
  425.         }
  426.         $selectedMonth $request->query->getInt('analysis_month', (int) date('m'));
  427.         $statsMonth $statsRepository->findBy(['year' => 2025'month' => $selectedMonth]);
  428.         $monthRevenue2025 0;
  429.         $monthProfit2025 0;
  430.         foreach ($statsMonth as $stat) {
  431.             $monthRevenue2025 += $stat->getTotalSalesQty() * $stat->getAvgSalesPrice();
  432.             $cost = (float) $stat->getAvgCostPrice();
  433.             $price = (float) $stat->getAvgSalesPrice();
  434.             $qty = (float) $stat->getTotalSalesQty();
  435.             $monthProfit2025 += ($price $cost) * $qty;
  436.         }
  437.         // --- 2026 Projeksiyon (Aylık Gelir Özeti) Hesaplaması (2025 Bazlı) ---
  438.         $strategicRevenue2026Breakdown = [];
  439.         $strategicRevenue2026Total 0;
  440.         // 2025 yılının tüm verilerini çekiyoruz
  441.         $allStats2025 $statsRepository->findBy(['year' => 2025]);
  442.         // 12 aylık boş bir dizi oluştur (1-12)
  443.         $monthlyRevenues array_fill(1120.0);
  444.         foreach ($allStats2025 as $stat) {
  445.             $m $stat->getMonth();
  446.             if ($m >= && $m <= 12) {
  447.                 $rev = (float) $stat->getTotalSalesQty() * (float) $stat->getAvgSalesPrice();
  448.                 $monthlyRevenues[$m] += $rev;
  449.             }
  450.         }
  451.         foreach ($monthlyRevenues as $m => $val) {
  452.             $strategicRevenue2026Breakdown[$m] = $val;
  453.             $strategicRevenue2026Total += $val;
  454.         }
  455.         // ---------------------------------------------------------------------
  456.         // --- 2026 Projeksiyon (Aylık Kâr Özeti) Hesaplaması (2025 Bazlı) ---
  457.         $strategicProfit2026Breakdown = [];
  458.         $strategicProfit2026Total 0;
  459.         $monthlyProfits array_fill(1120.0);
  460.         foreach ($allStats2025 as $stat) {
  461.             $m $stat->getMonth();
  462.             if ($m >= && $m <= 12) {
  463.                 $cost = (float) $stat->getAvgCostPrice();
  464.                 $price = (float) $stat->getAvgSalesPrice();
  465.                 $qty = (float) $stat->getTotalSalesQty();
  466.                 $monthlyProfits[$m] += ($price $cost) * $qty;
  467.             }
  468.         }
  469.         foreach ($monthlyProfits as $m => $val) {
  470.             $strategicProfit2026Breakdown[$m] = $val;
  471.             $strategicProfit2026Total += $val;
  472.         }
  473.         // --- 2026 Gerçekleşen Veriler (Actuals) ---
  474.         $allStats2026 $statsRepository->findBy(['year' => 2026]);
  475.         $actualRevenue2026Breakdown array_fill(1120.0);
  476.         $actualProfit2026Breakdown array_fill(1120.0);
  477.         $actualRevenue2026Total 0;
  478.         $actualProfit2026Total 0;
  479.         foreach ($allStats2026 as $stat) {
  480.             $m $stat->getMonth();
  481.             if ($m >= && $m <= 12) {
  482.                 $rev = (float) $stat->getTotalSalesQty() * (float) $stat->getAvgSalesPrice();
  483.                 $cost = (float) $stat->getAvgCostPrice();
  484.                 $price = (float) $stat->getAvgSalesPrice();
  485.                 $qty = (float) $stat->getTotalSalesQty();
  486.                 $prof = ($price $cost) * $qty;
  487.                 $actualRevenue2026Breakdown[$m] += $rev;
  488.                 $actualProfit2026Breakdown[$m] += $prof;
  489.             }
  490.         }
  491.         // Toplamları hesapla
  492.         $actualRevenue2026Total array_sum($actualRevenue2026Breakdown);
  493.         $actualProfit2026Total array_sum($actualProfit2026Breakdown);
  494.         // --- 2025 Ürün Bazlı Aylık Satış Raporu ---
  495.         // Veri Kaynakları:
  496.         // - Satış Miktarları: product_monthly_stats.total_sales_qty
  497.         // - Mevcut Stok: StockInfo tablosu
  498.         // - Birim Maliyet: product_monthly_stats.avg_cost_price (ağırlıklı ortalama)
  499.         $productSales2025 = [];
  500.         foreach ($allStats2025 as $stat) {
  501.             $product $stat->getProduct();
  502.             if ($product === null) {
  503.                 continue;
  504.             }
  505.             $pId $product->getId();
  506.             if (!isset($productSales2025[$pId])) {
  507.                 $productSales2025[$pId] = [
  508.                     'name' => $product->getName(),
  509.                     'code' => $product->getCode(),
  510.                     'months' => array_fill(1120),
  511.                     'total' => 0,
  512.                     // Mevcut Stok: StockInfo tablosundan
  513.                     'currentStock' => array_reduce($product->getStockInfos()->toArray(), function ($sum$item) {
  514.                         return $sum $item->getTotalQuantity();
  515.                     }, 0),
  516.                     // Maliyet hesaplaması için ara değerler
  517.                     'totalCostSum' => 0,  // Toplam maliyet (qty * cost)
  518.                     'totalQtyForCost' => 0// Toplam miktar (ağırlıklı ortalama için)
  519.                     'unitPrice' => 0  // Sonradan hesaplanacak
  520.                 ];
  521.             }
  522.             $m $stat->getMonth();
  523.             if ($m >= && $m <= 12) {
  524.                 $qty = (float) $stat->getTotalSalesQty();
  525.                 $avgCostPrice = (float) $stat->getAvgCostPrice();
  526.                 $productSales2025[$pId]['months'][$m] += $qty;
  527.                 $productSales2025[$pId]['total'] += $qty;
  528.                 // Ağırlıklı ortalama maliyet hesabı için: (qty * cost) toplamı
  529.                 if ($qty && $avgCostPrice 0) {
  530.                     $productSales2025[$pId]['totalCostSum'] += ($qty $avgCostPrice);
  531.                     $productSales2025[$pId]['totalQtyForCost'] += $qty;
  532.                 }
  533.             }
  534.         }
  535.         // Post-process: Ağırlıklı ortalama maliyeti hesapla ve derived values'ları oluştur
  536.         foreach ($productSales2025 as $pId => &$data) {
  537.             $currentStock $data['currentStock'];
  538.             $yearlySales $data['total'];
  539.             // Birim Fiyat: Ağırlıklı ortalama maliyet (product_monthly_stats'tan)
  540.             $unitPrice 0;
  541.             if ($data['totalQtyForCost'] > 0) {
  542.                 $unitPrice $data['totalCostSum'] / $data['totalQtyForCost'];
  543.             }
  544.             $data['unitPrice'] = $unitPrice;
  545.             // --- Yearly Plan Calculations ---
  546.             // Yıllık Satılan: $yearlySales (product_monthly_stats'tan toplam)
  547.             // Mevcut Stok: $currentStock (StockInfo'dan)
  548.             // Satılan - Stok: $yearlySales - $currentStock
  549.             // Sipariş Miktarı: Satılan - Stok (negatifse 0)
  550.             // Toplam Maliyet: Sipariş Miktarı * Birim Fiyat
  551.             $salesMinusStock $yearlySales $currentStock;
  552.             $orderQty max(0ceil($salesMinusStock)); // Negatif siparişi engelle
  553.             $yearlyTotalCost $orderQty $unitPrice;
  554.             $data['yearly'] = [
  555.                 'sales' => ceil($yearlySales),         // Yıllık Satılan
  556.                 'remaining' => ceil($salesMinusStock), // Satılan - Stok
  557.                 'order' => $orderQty,                  // Sipariş Miktarı
  558.                 'totalCost' => $yearlyTotalCost        // Toplam
  559.             ];
  560.             // --- Monthly Projection Calculations (Stok - Satılan) ---
  561.             $monthlyCalculations = [];
  562.             for ($m 1$m <= 12$m++) {
  563.                 $monthlySales $data['months'][$m];
  564.                 $monthlyRemaining ceil($currentStock $monthlySales);
  565.                 $monthlyOrder $monthlyRemaining;
  566.                 $monthlyTotalCost $monthlyOrder $unitPrice;
  567.                 $monthlyCalculations[$m] = [
  568.                     'sales' => $monthlySales,
  569.                     'remaining' => $monthlyRemaining,
  570.                     'order' => $monthlyOrder,
  571.                     'totalCost' => $monthlyTotalCost
  572.                 ];
  573.             }
  574.             $data['monthly_derived'] = $monthlyCalculations;
  575.             // Temizlik: Ara hesaplama alanlarını kaldır
  576.             unset($data['totalCostSum'], $data['totalQtyForCost']);
  577.         }
  578.         unset($data); // Break reference
  579.         // Çok satandan aza doğru sırala - uasort ile anahtarları koru (frontend için gerekli)
  580.         uasort($productSales2025, function ($a$b) {
  581.             return $b['total'] <=> $a['total'];
  582.         });
  583.         // Sort allProducts based on 2025 sales totals (Added for Product Projection Table)
  584.         usort($allProducts, function ($a$b) use ($productSales2025) {
  585.             $totalA $productSales2025[$a->getId()]['total'] ?? 0;
  586.             $totalB $productSales2025[$b->getId()]['total'] ?? 0;
  587.             return $totalB <=> $totalA// DESC
  588.         });
  589.         // --- 2025-2026 Inventory Turn & Sleeping Stock Analysis (Rolling 12 Months) ---
  590.         $inventoryAnalysis = [];
  591.         // 1. Fetch Lifecycle Stats (First Seen, All-time Sales/Purchases) for ALL products
  592.         $lifecycleStats $statsRepository->getProductLifecycleStats();
  593.         // Dinamik olarak son 12 ayı belirle
  594.         $rollingMonths = [];
  595.         $currentMonthObj = new \DateTimeImmutable('first day of this month');
  596.         for ($i 0$i 12$i++) {
  597.             $date $currentMonthObj->modify("-$i month");
  598.             $rollingMonths[] = [
  599.                 'year' => (int) $date->format('Y'),
  600.                 'month' => (int) $date->format('m')
  601.             ];
  602.         }
  603.         // Son 12 ayın verilerini toplu çek (Performans için)
  604.         $yearsToFetch array_unique(array_column($rollingMonths'year'));
  605.         $allRecentStats $statsRepository->findBy(['year' => $yearsToFetch]);
  606.         // Ürün bazlı grupla
  607.         $statsByProduct = [];
  608.         foreach ($allRecentStats as $s) {
  609.             if ($s->getProduct()) {
  610.                 $pId $s->getProduct()->getId();
  611.                 $statsByProduct[$pId][$s->getYear()][$s->getMonth()] = $s;
  612.             }
  613.         }
  614.         foreach ($allProducts as $product) {
  615.             // Sadece Tekil Ürün ve Paket Ürün olanları dahil et (Transport, Servis vb. hariç)
  616.             $type $product->getProductTypeEnum();
  617.             if ($type !== ProductTypeEnum::SINGLE_ITEM && $type !== ProductTypeEnum::BUNDLE) {
  618.                 continue;
  619.             }
  620.             $pId $product->getId();
  621.             // Mevcut Stok (Tüm depolardan)
  622.             $currentStock array_reduce($product->getStockInfos()->toArray(), function ($sum$item) {
  623.                 return $sum $item->getTotalQuantity();
  624.             }, 0);
  625.             $totalSalesLast12Months 0;
  626.             $totalSales3Months 0;
  627.             $totalSales6Months 0;
  628.             $inactiveMonthsCount 0// Last 8 months
  629.             $revenueLast12Months 0;
  630.             $costLast12Months 0;
  631.             // Rolling months dizisi tersten (en eskiden en yeniye) değil, en yeniden en eskiye doğru
  632.             foreach ($rollingMonths as $index => $period) {
  633.                 $s $statsByProduct[$pId][$period['year']][$period['month']] ?? null;
  634.                 $qty $s ? (float) $s->getTotalSalesQty() : 0;
  635.                 $totalSalesLast12Months += $qty;
  636.                 // Ek kolonlar için (3 Ay, 6 Ay)
  637.                 if ($index 3) {
  638.                     $totalSales3Months += $qty;
  639.                 }
  640.                 if ($index 6) {
  641.                     $totalSales6Months += $qty;
  642.                 }
  643.                 // Hareketsizlik kontrolü (Son 8 ay için)
  644.                 if ($index 8) {
  645.                     if ($qty <= 0) {
  646.                         $inactiveMonthsCount++;
  647.                     }
  648.                 }
  649.                 if ($s) {
  650.                     $revenueLast12Months += $qty * (float) $s->getAvgSalesPrice();
  651.                     $costLast12Months += $qty * (float) $s->getAvgCostPrice();
  652.                 }
  653.             }
  654.             // Eğer verilerde maliyet/fiyat yoksa ürün kartından al
  655.             $avgCostLast12 $totalSalesLast12Months $costLast12Months $totalSalesLast12Months : (float) ($product->getPurchasePrice() ?? 0);
  656.             $avgPriceLast12 $totalSalesLast12Months $revenueLast12Months $totalSalesLast12Months : (float) ($product->getSalePrice() ?? 0);
  657.             // 1. Satış Hızı (Velocity) - Aylık Ortalama
  658.             $velocity $totalSalesLast12Months 12;
  659.             // Lifecycle Verileri
  660.             $lStats $lifecycleStats[$pId] ?? null;
  661.             $isNewProduct false;
  662.             $allTimeSellThrough 0;
  663.             if ($lStats) {
  664.                 // Yeni Ürün Kontrolü: < 3 Ay (First Seen Date)
  665.                 $firstSeenDate = new \DateTime($lStats['firstSeen'] . '-01'); // YYYY-MM-01
  666.                 $threeMonthsAgo = (new \DateTime())->modify('-3 months');
  667.                 if ($firstSeenDate $threeMonthsAgo) {
  668.                     $isNewProduct true;
  669.                 }
  670.                 // Sell-Through Rate (All Time)
  671.                 if ($lStats['totalPurchases'] > 0) {
  672.                     $allTimeSellThrough $lStats['totalSales'] / $lStats['totalPurchases'];
  673.                 }
  674.             } else {
  675.                 // İstatistik tablosunda hiç kaydı yoksa (veya eski değilse) Yeni Ürün kabul edebiliriz
  676.                 $isNewProduct true;
  677.             }
  678.             // 2. Sleeping Stock & Status Logic (Advanced)
  679.             $sleepingStock 0;
  680.             $status 'Normal';
  681.             if ($isNewProduct) {
  682.                 $status 'Yeni Ürün';
  683.             } else {
  684.                 // Atıl Stok Kontrolü: 8 aydır satış yok
  685.                 if ($inactiveMonthsCount >= && $currentStock 0) {
  686.                     $status 'Atıl Stok (8 Ay Hareketsiz)';
  687.                     $sleepingStock $currentStock// Hepsi uyuyan kabul edilir
  688.                 } else {
  689.                     // Uyuyan Stok (Coverage & Turnover Logic)
  690.                     // Stok bizi kaç ay idare eder? (Stock Coverage)
  691.                     $stockCoverageMonths $velocity ? ($currentStock $velocity) : 999;
  692.                     // Kural 1: 12 Aydan fazla yetecek stok varsa -> Fazlası uyuyandır
  693.                     if ($stockCoverageMonths 12) {
  694.                         $neededStock $velocity 12// 1 senelik stok makul
  695.                         $sleepingStock max(0$currentStock $neededStock);
  696.                         if ($sleepingStock 0) {
  697.                             $status 'Uyuyan Stok';
  698.                         }
  699.                     }
  700.                     // Kural 2: 6 Aydan fazla yetecek stok var VE Satış Performansı (Sell-Through) %20'nin altındaysa
  701.                     elseif ($stockCoverageMonths && $allTimeSellThrough 0.20) {
  702.                         $neededStock $velocity 6// 6 aylık stok makul
  703.                         $sleepingStock max(0$currentStock $neededStock);
  704.                         if ($sleepingStock 0) {
  705.                             $status 'Uyuyan Stok (Düşük Performans)';
  706.                         }
  707.                     } elseif ($currentStock <= ($velocity 1.5) && $velocity 0) {
  708.                         $status 'Hızlı Dönüş (Kritik)';
  709.                     } else {
  710.                         // Verimlilik kontrolü ekle: Eğer stok varsa ama verimlilik/satış hızı çok düşükse Normal demeyelim
  711.                         if ($currentStock && $velocity 0.1) {
  712.                             $status 'Takip Gereken (Düşük Hız)';
  713.                         }
  714.                     }
  715.                 }
  716.             }
  717.             // 4. Yatırım Verimliliği (ROI)
  718.             // Bağlı Sermaye: Mevcut Stok * Maliyet
  719.             $tiedCapital $currentStock $avgCostLast12;
  720.             // Verimlilik: Son 12 aylık ciro / Bağlı sermaye (veya benzeri bir oran)
  721.             $efficiency $tiedCapital ? ($revenueLast12Months $tiedCapital) : ($totalSalesLast12Months 99 0);
  722.             if ($currentStock || $totalSalesLast12Months 0) {
  723.                 $inventoryAnalysis[] = [
  724.                     'id' => $product->getId(),
  725.                     'name' => $product->getName(),
  726.                     'code' => $product->getCode(),
  727.                     'endStock' => $currentStock,
  728.                     'totalSales' => $totalSalesLast12Months,
  729.                     'totalSales3Months' => $totalSales3Months,
  730.                     'totalSales6Months' => $totalSales6Months,
  731.                     'velocity' => $velocity,
  732.                     'sleepingStock' => $sleepingStock,
  733.                     'status' => $status,
  734.                     'avgCost' => $avgCostLast12,
  735.                     'avgPrice' => $avgPriceLast12,
  736.                     'sleepingStockCost' => $sleepingStock $avgCostLast12,
  737.                     'sleepingStockRevenue' => $sleepingStock $avgPriceLast12,
  738.                     'efficiency' => $efficiency,
  739.                     'inactiveMonths' => $inactiveMonthsCount
  740.                 ];
  741.             }
  742.         }
  743.         // Uyuyan stok miktarına göre azalan sırala
  744.         usort($inventoryAnalysis, function ($a$b) {
  745.             return $b['sleepingStock'] <=> $a['sleepingStock'];
  746.         });
  747.         // --- Budget Planning Data (Aylık Bütçe Detayı) ---
  748.         $budgetPlanningData $financialService->generateFinancialForecast(20252026true);
  749.         // --------------------------------------------
  750.         return $this->render('admin/dashboard/index.html.twig', [
  751.             'controller_name' => 'DashboardController',
  752.             'mountly' => CurrencyHelper::convertToCurrency($mountly),
  753.             'weekly' => CurrencyHelper::convertToCurrency($weekly),
  754.             'profitMonthly' => $profitMothly,
  755.             'profitWeekly' => $profitWeekly,
  756.             'yearly' => $yearly,
  757.             'inTransits' => $inTransitTransferNumber,
  758.             'earningInPeriots' => $array,
  759.             'sallers' => $this->userService->getTotalUsersEarns(),
  760.             'allSallers' => $allSallers,
  761.             'allWarehouses' => $allWarehouses,
  762.             'allProducts' => $allProducts,
  763.             'periyotlar' => $periyotlar,
  764.             'urunler' => $urun,
  765.             'monthsTr' => $monthsTr,
  766.             'sellers' => $sellers,
  767.             'sellersDetail' => $sellersDetail,
  768.             'warehouses' => $this->warehouseService->getAll(),
  769.             'lowStockProducts' => $lowStockProducts,
  770.             // New Variables (Strategic)
  771.             'strategic_2025_revenue' => $totalRevenue2025,
  772.             'strategic_2025_profit' => $totalProfit2025,
  773.             'strategic_month_revenue' => $monthRevenue2025,
  774.             'strategic_month_profit' => $monthProfit2025,
  775.             'selected_analysis_month' => $selectedMonth,
  776.             'strategic_revenue_2026_breakdown' => $strategicRevenue2026Breakdown,
  777.             'strategic_revenue_2026_total' => $strategicRevenue2026Total,
  778.             // New Variables (Actuals & Profit Projection)
  779.             'strategic_profit_2026_breakdown' => $strategicProfit2026Breakdown,
  780.             'strategic_profit_2026_total' => $strategicProfit2026Total,
  781.             'actual_revenue_2026_breakdown' => $actualRevenue2026Breakdown,
  782.             'actual_revenue_2026_total' => $actualRevenue2026Total,
  783.             'actual_profit_2026_breakdown' => $actualProfit2026Breakdown,
  784.             'actual_profit_2026_total' => $actualProfit2026Total,
  785.             'product_sales_2025' => $productSales2025,
  786.             'inventory_analysis' => $inventoryAnalysis,
  787.             // Budget Planning Data (Aylık Bütçe Detayı)
  788.             'budget_planning' => $budgetPlanningData,
  789.             'allMeasurementUnits' => $this->entityManager->getRepository(\App\Entity\MeasurementUnits::class)->findAll(),
  790.             'allMeasurements' => $this->entityManager->getRepository(\App\Entity\Measurements::class)->findAll(),
  791.             'uniqueProductCodes' => $uniqueProductCodes,
  792.         ]);
  793.     }
  794.     public function getLowStockProducts()
  795.     {
  796.         $qb $this->entityManager->getRepository(StockInfo::class)->createQueryBuilder('si');
  797.         $qb
  798.             ->leftJoin('si.product''p')
  799.             ->leftJoin('si.warehouse''w')
  800.             ->leftJoin('p.measurementUnit''mu')
  801.             ->leftJoin('p.measurement''m')
  802.             ->where(
  803.                 $qb->expr()->orX(
  804.                     // Eğer symbol 'SET' ise totalQuantity 200 altı olanları getir
  805.                     $qb->expr()->andX(
  806.                         $qb->expr()->eq('mu.symbol'':setSymbol'),
  807.                         $qb->expr()->lt('si.totalQuantity'':setThreshold')
  808.                     ),
  809.                     // Aksi durumda totalQuantity 300 altı olanları getir
  810.                     $qb->expr()->andX(
  811.                         $qb->expr()->neq('mu.symbol'':setSymbol'),
  812.                         $qb->expr()->lt('si.totalQuantity'':defaultThreshold')
  813.                     )
  814.                 )
  815.             )
  816.             ->setParameter('setSymbol''SET')
  817.             ->setParameter('setThreshold'200)
  818.             ->setParameter('defaultThreshold'300)
  819.             ->select(
  820.                 'p.id as productId',
  821.                 'p.name as name',
  822.                 'p.code as productCode',
  823.                 'm.name as measurement',
  824.                 'si.totalQuantity as totalQuantity',
  825.                 'si.id as siId',
  826.                 'w.name as warehouseName',
  827.                 'mu.name as measurementUnit',
  828.                 'w.id as warehouseId',
  829.             )
  830.             ->orderBy('si.totalQuantity''desc');
  831.         $result $qb->getQuery()->getResult();
  832.         $dates $this->getDatesFromOneYear();
  833.         for ($i 0$i count($result); $i++) {
  834.             foreach ($dates as $date) {
  835.                 $product $this->productServiceImpl->getEntityId($result[$i]['productId']);
  836.                 $price $this->salesProductsServiceImpl->getQuantityOfProductSoldBy($date['start'], $date['end'], $product$result[$i]['warehouseId']);
  837.                 $result[$i]['quantities'][] = $price;
  838.                 $result[$i]['uuid'] = str_replace("."""uniqid(''true));
  839.             }
  840.         }
  841.         //        dd($result);
  842. //        dd($this->getDatesFromOneYear());
  843.         return $result;
  844.     }
  845.     #[Route('/admin/dashboard/product/sold/details/{productId}/{warehouseId}'name'admin_dashboard_product_sold_details')]
  846.     public function getQuantityOfSoldProductDetails(Request $request$productId$warehouseId)
  847.     {
  848.         $dates $this->getDatesFromOneYear();
  849.         $product $this->productServiceImpl->getEntityId($productId);
  850.         $warehouse null;
  851.         if ($warehouseId != null && $warehouseId != 0) {
  852.             $warehouse $this->warehouseService->getEntityById($warehouseId);
  853.         }
  854.         $result = [];
  855.         for ($i 0$i count($dates); $i++) {
  856.             $month DateUtil::getMonthOfTurkish((int) (new \DateTimeImmutable($dates[$i]['start']))->format('m'));
  857.             $quantity $this->salesProductsServiceImpl->getQuantityOfProductSoldBy(
  858.                 $dates[$i]['start'],
  859.                 $dates[$i]['end'],
  860.                 $product,
  861.                 $warehouse
  862.             );
  863.             if ($quantity == null)
  864.                 $quantity 0.00;
  865.             else
  866.                 $quantity CurrencyHelper::roundUp($quantity);
  867.             $result[$month] = [
  868.                 'quantity' => $quantity,
  869.                 'measurement' => $product->getMeasurementUnit()->getSymbol(),
  870.                 'startDate' => $dates[$i]['start'],
  871.                 'endDate' => $dates[$i]['end']
  872.             ];
  873.         }
  874.         return new JsonResponse($result);
  875.     }
  876.     public function getDatesFromOneYear()
  877.     {
  878.         $dates = [];
  879.         $today = new \DateTimeImmutable();
  880.         // Son bir yıl boyunca her ay için başlangıç ve bitiş tarihi
  881.         for ($i 0$i 12$i++) {
  882.             // Geçmişe doğru git
  883.             $monthStart $today->modify("-$i month")->modify('first day of this month');
  884.             $monthEnd $monthStart->modify('last day of this month');
  885.             $dates[] = [
  886.                 'month' => 12 $i,
  887.                 'start' => $monthStart->format('Y-m-d'), // Başlangıç tarihi
  888.                 'end' => $monthEnd->format('Y-m-d')     // Bitiş tarihi
  889.             ];
  890.         }
  891.         return $dates;
  892.     }
  893.     public function getSalesBySeller($startDate null$finishDate null$products null$sellerIds null$warehouseId null)
  894.     {
  895.         $year date('Y');
  896.         if ($startDate == null)
  897.             $startDate = new \DateTimeImmutable('first day of january ' $year);
  898.         else {
  899.             $startDate \DateTimeImmutable::createFromFormat('Y-m-d'$startDate);
  900.         }
  901.         if ($finishDate == null)
  902.             $finishDate = new \DateTimeImmutable('last day of december ' $year);
  903.         else {
  904.             $finishDate \DateTimeImmutable::createFromFormat('Y-m-d'$finishDate);
  905.         }
  906.         $userRepo $this->entityManager->getRepository(User::class);
  907.         $canSeeHidden $this->canCurrentUserSeeHiddenSales();
  908.         $qb $userRepo->createQueryBuilder('u')
  909.             ->leftJoin('u.sales''s')
  910.             ->leftJoin('s.warehouse''w')
  911.             ->leftJoin('s.productsSolds''ps')
  912.             ->leftJoin('ps.product''p')
  913.             ->where('s.salesDate >= :startDate')
  914.             ->andWhere('s.salesDate <= :finishDate')
  915.             ->setParameter('startDate'$startDate)
  916.             ->setParameter('finishDate'$finishDate);
  917.         if (!$canSeeHidden) {
  918.             $qb->andWhere('s.visible = :visible')
  919.                 ->setParameter('visible'true);
  920.         }
  921.         if ($sellerIds != null) {
  922.             $qb->andWhere(
  923.                 $qb->expr()->in('u.id'$sellerIds)
  924.             );
  925.         }
  926.         if ($products != null) {
  927.             $qb->andWhere(
  928.                 $qb->expr()->in('product.id'$products)
  929.             );
  930.         }
  931.         if ($warehouseId != null && $warehouseId != 0) {
  932.             $qb->andWhere(
  933.                 $qb->expr()->eq('w.id'$warehouseId)
  934.             );
  935.         }
  936.         $result $qb->getQuery()->getResult();
  937.         $array = [];
  938.         /**
  939.          * @var User $user
  940.          */
  941.         foreach ($result as $user) {
  942.             $totalPurchasePrice 0.00;
  943.             $totalProfit 0.00;
  944.             foreach ($user->getSales() as $sale) {
  945.                 if (!$canSeeHidden && !$sale->isVisible()) {
  946.                     continue;
  947.                 }
  948.                 if ($sale->getSalesDate() >= $startDate && $sale->getSalesDate() <= $finishDate) {
  949.                     foreach ($sale->getProductsSolds() as $productsSold) {
  950.                         $totalProfit += $this->salesProductsServiceImpl->calculateProfitByProductSold($productsSold);
  951.                         $totalPurchasePrice += $productsSold->getTotalPuchasePrice();
  952.                     }
  953.                 }
  954.             }
  955.             $array[] = [
  956.                 'userId' => $user->getId(),
  957.                 'startDate' => $startDate->format('d-m-Y'),
  958.                 'finishDate' => $finishDate->format('d-m-Y'),
  959.                 'username' => $user->getUsername(),
  960.                 'email' => $user->getEmail(),
  961.                 'totalPurchasePrice' => $totalPurchasePrice,
  962.                 'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
  963.                     $startDate,
  964.                     $finishDate,
  965.                     $warehouseId,
  966.                     $user->getId()
  967.                 )
  968.             ];
  969.         }
  970.         return $array;
  971.     }
  972.     #[Route('/get-product-by-productname'name'get_product_by_product_name')]
  973.     public function getProductByProductName(Request $request)
  974.     {
  975.         $search $request->get('search');
  976.         $qb $this->entityManager->getRepository(Product::class)
  977.             ->createQueryBuilder('p')
  978.             ->where('p.name LIKE :s')
  979.             ->setParameter('s''%' $search '%')
  980.             ->getQuery()
  981.             ->getResult();
  982.         return $this->json($qb);
  983.     }
  984.     #[Route('/dashboard/weeaks-earning/{warehouseId}'name'dashboard_week_earning')]
  985.     public function getWeekEarningByWeek(Request $requestEntityManagerInterface $entityManager$warehouseId)
  986.     {
  987.         $firstDayOfWeek trim(explode('*'$request->get('week'))[0]);
  988.         $lastDayOfWeek trim(explode('*'$request->get('week'))[1]);
  989.         $firstDayOfWeek \DateTimeImmutable::createFromFormat('Y-m-d'$firstDayOfWeek);
  990.         $lastDayOfWeek \DateTimeImmutable::createFromFormat('Y-m-d'$lastDayOfWeek);
  991.         $productsSoldRepo $entityManager->getRepository(ProductsSold::class);
  992.         $canSeeHidden $this->canCurrentUserSeeHiddenSales();
  993.         $qb $productsSoldRepo->createQueryBuilder('c')
  994.             ->leftJoin('c.sales''sales')
  995.             ->leftJoin('c.product''product')
  996.             ->where('sales.salesDate >= :firstDayOfWeek')
  997.             ->andWhere('sales.salesDate <= :lastDayOfWeek')
  998.             ->setParameters(
  999.                 [
  1000.                     'firstDayOfWeek' => $firstDayOfWeek,
  1001.                     'lastDayOfWeek' => $lastDayOfWeek
  1002.                 ]
  1003.             );
  1004.         if (!$canSeeHidden) {
  1005.             $qb->andWhere('sales.visible = :visible')
  1006.                 ->setParameter('visible'true);
  1007.         }
  1008.         $qb $qb->getQuery()->getResult();
  1009.         $period = new DatePeriod(
  1010.             date_create_from_format('Y-m-d'$firstDayOfWeek->format('Y-m-d')),
  1011.             new DateInterval('P1D'),
  1012.             date_create_from_format('Y-m-d'$lastDayOfWeek->format('Y-m-d'))->add(new DateInterval('P1D'))
  1013.         );
  1014.         $days = [];
  1015.         foreach ($period as $key => $value) {
  1016.             $days[] = $value->format('Y-m-d');
  1017.         }
  1018.         if ($warehouseId == 0)
  1019.             $warehouseId null;
  1020.         return new JsonResponse(
  1021.             [
  1022.                 'totalEarning' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate(
  1023.                     $firstDayOfWeek,
  1024.                     $lastDayOfWeek,
  1025.                     $warehouseId,
  1026.                 ),
  1027.                 'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
  1028.                     $firstDayOfWeek,
  1029.                     $lastDayOfWeek,
  1030.                     $warehouseId,
  1031.                 ),
  1032.                 'days' => $days,
  1033.             ]
  1034.         );
  1035.     }
  1036.     #[Route('/dashboard/day-earning/{warehouseId}'name'dashboard_day_earning'methods: ['GET''POST'])]
  1037.     public function getEarningByDay(Request $requestEntityManagerInterface $entityManager$warehouseId)
  1038.     {
  1039.         $day $request->get('day');
  1040.         $day \DateTimeImmutable::createFromFormat('Y-m-d'$day);
  1041.         $productsSoldRepo $entityManager->getRepository(ProductsSold::class);
  1042.         $canSeeHidden $this->canCurrentUserSeeHiddenSales();
  1043.         $qb $productsSoldRepo->createQueryBuilder('c')
  1044.             ->leftJoin('c.sales''sales')
  1045.             ->leftJoin('c.product''product')
  1046.             ->where('sales.salesDate = :day')
  1047.             ->setParameter('day'$day->format('Y-m-d'));
  1048.         if (!$canSeeHidden) {
  1049.             $qb->andWhere('sales.visible = :visible')
  1050.                 ->setParameter('visible'true);
  1051.         }
  1052.         $qb $qb->getQuery()->getResult();
  1053.         if ($warehouseId == 0)
  1054.             $warehouseId null;
  1055.         return new JsonResponse(
  1056.             [
  1057.                 'totalEarning' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate(
  1058.                     $day,
  1059.                     $day,
  1060.                     $warehouseId
  1061.                 ),
  1062.                 'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
  1063.                     $day,
  1064.                     $day,
  1065.                     $warehouseId
  1066.                 ),
  1067.             ]
  1068.         );
  1069.     }
  1070.     #[Route('/dashboard/year-earning/{warehouseId}'name'dashboard_year_earning')]
  1071.     public function getEarningByYear(Request $requestEntityManagerInterface $entityManager$warehouseId)
  1072.     {
  1073.         $year $request->get('year');
  1074.         $firstDayOfYear = new \DateTimeImmutable('first day of january ' $year);
  1075.         $lastDayOfYear = new \DateTimeImmutable('last day of december ' $year ' 23:59:59');
  1076.         //        $productsSoldRepo = $entityManager->getRepository(ProductsSold::class);
  1077. //        $qb = $productsSoldRepo->createQueryBuilder('c')
  1078. //            ->leftJoin('c.sales', 'sales')
  1079. //            ->leftJoin('c.product', 'product')
  1080. //            ->where('sales.salesDate >= :firstDayOfYear')
  1081. //            ->setParameter('firstDayOfYear',$firstDayOfYear)
  1082. //            ->andWhere('sales.salesDate <= :lastDayOfYear')
  1083. //            ->setParameter('lastDayOfYear', $lastDayOfYear);
  1084. //
  1085. //        if($request->get('all-warehouses') == null){
  1086. //
  1087. //            $qb->andWhere('sales.warehouse = :warehouse')
  1088. //                ->setParameter('warehouse',$this->warehouseService->getMainWarehouse());
  1089. //        }
  1090. //
  1091. //        $qb = $qb->getQuery()->getResult();
  1092. //
  1093. //        $totalEarningsArr = array_map(function ($val){
  1094. //            return $val->getTotalPuchasePrice();
  1095. //        }, $qb);
  1096. //
  1097. //        $totalEarning = 0.00;
  1098. //        foreach ($totalEarningsArr as $totalEarn){
  1099. //            $totalEarning += $totalEarn;
  1100. //        }
  1101. //
  1102. //        $profits = array_map(function ($val){
  1103. //            return $val->getTotalPuchasePrice() - $val->getTotalPrice();
  1104. //        },$qb);
  1105. //
  1106. //
  1107. //        $totalProfit = 0.00;
  1108. //
  1109. //        foreach ($profits as $profit){
  1110. //            $totalProfit += $profit;
  1111. //        }
  1112.         return new JsonResponse(
  1113.             [
  1114.                 'totalEarning' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDateAndByWarehouse(
  1115.                     $firstDayOfYear,
  1116.                     $lastDayOfYear,
  1117.                     $warehouseId == null $warehouseId,
  1118.                 ),
  1119.                 'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
  1120.                     $firstDayOfYear,
  1121.                     $lastDayOfYear,
  1122.                     $warehouseId == null $warehouseId,
  1123.                 ),
  1124.             ]
  1125.         );
  1126.     }
  1127.     #[Route('/dashboard/month-earning/{warehouseId}'name'dashboard_month_earning')]
  1128.     public function getEarningByMonth(Request $requestEntityManagerInterface $entityManager$warehouseId)
  1129.     {
  1130.         if ($request->get('year') != null) {
  1131.             $year $request->get('year');
  1132.         } else {
  1133.             $year date('Y');
  1134.         }
  1135.         $months = ['January''February''March''April''May''June''July''August''September''October''November''December'];
  1136.         $month $request->get('month');
  1137.         $firstDayOfMounth = (new \DateTimeImmutable('first day of ' $months[$month 1] . ' ' $year))->format('Y-m-d');
  1138.         $lastDayOfMount = (new \DateTimeImmutable('last day of ' $months[$month 1] . ' ' $year))->format('Y-m-d');
  1139.         $firstWeekStartDay $firstDayOfMounth;
  1140.         $firstWeekFinishDay = (new \DateTime('+6 days ' $firstDayOfMounth ' ' $year))->format('Y-m-d');
  1141.         $secondWeekStartDay = (new \DateTime('+1 days ' $firstWeekFinishDay ' ' $year))->format('Y-m-d');
  1142.         $secondWeekFinishDay = (new \DateTime('+6 days ' $secondWeekStartDay ' ' $year))->format('Y-m-d');
  1143.         $thirdWeekStartDay = (new \DateTime('+1 days ' $secondWeekFinishDay ' ' $year))->format('Y-m-d');
  1144.         $thirdWeekFinishDay = (new \DateTime('+6 days ' $thirdWeekStartDay ' ' $year))->format('Y-m-d');
  1145.         $fourthWeekStartDate = (new \DateTime('+1 days ' $thirdWeekFinishDay ' ' $year))->format('Y-m-d');
  1146.         $fourthWeekFinishDate $lastDayOfMount;
  1147.         $firstWeek $firstWeekStartDay " * " $firstWeekFinishDay;
  1148.         $secondWeek $secondWeekStartDay " * " $secondWeekFinishDay;
  1149.         $thirdWeek $thirdWeekStartDay " * " $thirdWeekFinishDay;
  1150.         $fourthWeek $fourthWeekStartDate " * " $fourthWeekFinishDate;
  1151.         $productsSoldRepo $entityManager->getRepository(ProductsSold::class);
  1152.         $canSeeHidden $this->canCurrentUserSeeHiddenSales();
  1153.         $qb $productsSoldRepo->createQueryBuilder('c')
  1154.             ->leftJoin('c.sales''sales')
  1155.             ->leftJoin('c.product''product')
  1156.             ->where('sales.salesDate >= :firstDayOfMonth')
  1157.             ->andWhere('sales.salesDate <= :lastDayOfMonth')
  1158.             ->setParameters(
  1159.                 [
  1160.                     'firstDayOfMonth' => $firstDayOfMounth,
  1161.                     'lastDayOfMonth' => $lastDayOfMount
  1162.                 ]
  1163.             )
  1164.             ->orderBy('sales.salesDate''DESC');
  1165.         if (!$canSeeHidden) {
  1166.             $qb->andWhere('sales.visible = :visible')
  1167.                 ->setParameter('visible'true);
  1168.         }
  1169.         $qb $qb->getQuery()->getResult();
  1170.         $data = [];
  1171.         foreach ($qb as $val) {
  1172.             $data[] = [
  1173.                 $val->getProduct()->getCode(),
  1174.                 $val->getProductName(),
  1175.                 $val->getQuantity(),
  1176.                 $val->getUnitPriceFob(),
  1177.                 $val->getUnitPriceNavlun(),
  1178.                 $val->getTotalUnitPrice(),
  1179.                 $val->getTotalPrice(),
  1180.                 $val->getTotalPuchasePrice()
  1181.             ];
  1182.         }
  1183.         $startDate = (new \DateTimeImmutable('first day of ' $months[$month 1] . ' ' $year));
  1184.         $finishDate = (new \DateTimeImmutable('last day of ' $months[$month 1] . ' ' $year));
  1185.         if ($warehouseId == 0)
  1186.             $warehouseId null;
  1187.         return new JsonResponse(
  1188.             [
  1189.                 'profit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate($startDate$finishDate$warehouseId),
  1190.                 'totalPrices' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate($startDate$finishDate$warehouseId),
  1191.                 'data' => $data,
  1192.                 'firstWeek' => $firstWeek,
  1193.                 'secondWeek' => $secondWeek,
  1194.                 'thirdWeek' => $thirdWeek,
  1195.                 'fourthWeek' => $fourthWeek
  1196.             ]
  1197.         );
  1198.     }
  1199.     private function getProductsByPeriot($startdate$finishDate$product$warehouse$seller)
  1200.     {
  1201.         $repo $this->entityManager->getRepository(Sales::class);
  1202.         $canSeeHidden $this->canCurrentUserSeeHiddenSales();
  1203.         $result $repo->createQueryBuilder('c')
  1204.             ->leftJoin('c.productsSolds''productsSolds')
  1205.             ->leftJoin('productsSolds.product''product')
  1206.             ->leftJoin('c.warehouse''warehouse');
  1207.         if ($startdate != null) {
  1208.             $result->where('c.salesDate >= :startDate')
  1209.                 ->setParameter('startDate'$startdate);
  1210.         }
  1211.         if ($finishDate != null) {
  1212.             $result->andWhere('c.salesDate <= :finishDate')
  1213.                 ->setParameter('finishDate'$finishDate);
  1214.         }
  1215.         if ($product != 0) {
  1216.             $result->andWhere('product.id = :product')
  1217.                 ->setParameter('product'$product);
  1218.         }
  1219.         if ($warehouse != null) {
  1220.             $result->andWhere('warehouse.id = :warehouseId')
  1221.                 ->setParameter('warehouseId'$warehouse);
  1222.         }
  1223.         if ($seller != 0) {
  1224.             $result->andWhere('c.seller = :seller')
  1225.                 ->setParameter('seller'$seller);
  1226.         }
  1227.         if (!$canSeeHidden) {
  1228.             $result->andWhere('c.visible = :visible')
  1229.                 ->setParameter('visible'true);
  1230.         }
  1231.         $result
  1232.             ->select(['product.name as productName''sum(c.totalPurchasePrice) totalAmount']);
  1233.         return $result->getQuery()->getResult();
  1234.     }
  1235.     private function canCurrentUserSeeHiddenSales(): bool
  1236.     {
  1237.         $user $this->getUser();
  1238.         if (!$user) {
  1239.             return false;
  1240.         }
  1241.         $identifier null;
  1242.         if (method_exists($user'getUserIdentifier')) {
  1243.             $identifier $user->getUserIdentifier();
  1244.         } elseif (method_exists($user'getUsername')) {
  1245.             $identifier $user->getUsername();
  1246.         }
  1247.         if ($identifier === null) {
  1248.             return false;
  1249.         }
  1250.         return in_array(strtolower((string) $identifier), ['gizli@kullanici.com''azad@azad.com']);
  1251.     }
  1252.     #[Route('/{locale}'name'app_change_locale'priority: -10)]
  1253.     public function changeLocale(Request $request$localeRouterInterface $router)
  1254.     {
  1255.         $params $this->getRefererParams($request$router);
  1256.         $locales $this->getParameter('kernel.enabled_locales');
  1257.         if (in_array($locale$locales)) {
  1258.             if ($this->getUser())
  1259.                 $this->userService->changeLocale($this->getUser(), $locale);
  1260.             $request->getSession()->set('_locale'$locale);
  1261.             return $this->redirect($params);
  1262.         }
  1263.         $this->createNotFoundException(printf('%s locale is not avaible'$locale));
  1264.         return $this->redirectToRoute('app_panel_dashboard');
  1265.     }
  1266.     #[Route('/dashboard/product-sales-2025-ajax'name'get_product_sales_2025_ajax')]
  1267.     public function getProductSales2025Ajax(Request $requestProductRepository $productRepositoryProductMonthlyStatsRepository $statsRepo\App\Service\Analysis\SalesSeasonalityService $seasonalityService\App\Repository\SettingsRepository $settingsRepository\App\Service\Analysis\StrategicForecastingService $forecastingService): JsonResponse
  1268.     {
  1269.         // Client-side DataTable - tüm veriyi döndürüyoruz (pagination yok)
  1270.         $searchParams $_GET['search'] ?? [];
  1271.         $searchValue $searchParams['value'] ?? null;
  1272.         $qb $productRepository->createQueryBuilder('p')
  1273.             ->leftJoin(\App\Entity\ProductMonthlyStats::class, 's'\Doctrine\ORM\Query\Expr\Join::WITH's.product = p.id AND s.year = 2025')
  1274.             ->leftJoin('p.measurementUnit''mu')
  1275.             ->groupBy('p.id')
  1276.             ->select('p.id''p.name''p.code''mu.symbol as unitSymbol')
  1277.             ->addSelect('SUM(CASE WHEN s.month = 1 THEN s.totalSalesQty ELSE 0 END) as m1')
  1278.             ->addSelect('SUM(CASE WHEN s.month = 2 THEN s.totalSalesQty ELSE 0 END) as m2')
  1279.             ->addSelect('SUM(CASE WHEN s.month = 3 THEN s.totalSalesQty ELSE 0 END) as m3')
  1280.             ->addSelect('SUM(CASE WHEN s.month = 4 THEN s.totalSalesQty ELSE 0 END) as m4')
  1281.             ->addSelect('SUM(CASE WHEN s.month = 5 THEN s.totalSalesQty ELSE 0 END) as m5')
  1282.             ->addSelect('SUM(CASE WHEN s.month = 6 THEN s.totalSalesQty ELSE 0 END) as m6')
  1283.             ->addSelect('SUM(CASE WHEN s.month = 7 THEN s.totalSalesQty ELSE 0 END) as m7')
  1284.             ->addSelect('SUM(CASE WHEN s.month = 8 THEN s.totalSalesQty ELSE 0 END) as m8')
  1285.             ->addSelect('SUM(CASE WHEN s.month = 9 THEN s.totalSalesQty ELSE 0 END) as m9')
  1286.             ->addSelect('SUM(CASE WHEN s.month = 10 THEN s.totalSalesQty ELSE 0 END) as m10')
  1287.             ->addSelect('SUM(CASE WHEN s.month = 11 THEN s.totalSalesQty ELSE 0 END) as m11')
  1288.             ->addSelect('SUM(CASE WHEN s.month = 12 THEN s.totalSalesQty ELSE 0 END) as m12')
  1289.             ->addSelect('SUM(s.totalSalesQty) as total');
  1290.         // Arama Filtresi (opsiyonel - client-side yapıyor ama server-side de filtre olabilir)
  1291.         if ($searchValue) {
  1292.             $qb->where('p.name LIKE :search OR p.code LIKE :search')
  1293.                 ->setParameter('search''%' $searchValue '%');
  1294.         }
  1295.         // Varsayılan sıralama
  1296.         $qb->orderBy('total''DESC');
  1297.         // TÜM VERİYİ ÇEK (Client-side için pagination yok)
  1298.         $dbResults $qb->getQuery()->getResult();
  1299.         // --- STOK VERİSİNİ OPTİMİZE EDEREK ÇEKME (Bulk Fetch) ---
  1300.         $productIds = [];
  1301.         foreach ($dbResults as $r) {
  1302.             $productIds[] = $r['id'];
  1303.         }
  1304.         $stockMap = [];
  1305.         if (!empty($productIds)) {
  1306.             $stockResults $this->entityManager->getRepository(\App\Entity\StockInfo::class)->createQueryBuilder('si')
  1307.                 ->select('IDENTITY(si.product) as pid, SUM(si.totalQuantity) as totalStock')
  1308.                 ->where('si.product IN (:pids)')
  1309.                 ->setParameter('pids'$productIds)
  1310.                 ->groupBy('si.product')
  1311.                 ->getQuery()
  1312.                 ->getResult();
  1313.             foreach ($stockResults as $sr) {
  1314.                 $stockMap[$sr['pid']] = (float) $sr['totalStock'];
  1315.             }
  1316.         }
  1317.         // --------------------------------------------------------
  1318.         // Para Birimi Ayarlarını Çek
  1319.         $settings $settingsRepository->findOneBy([]);
  1320.         $currencyCode $settings $settings->getCurrency() : 'EUR';
  1321.         $currencySymbol = match ($currencyCode) {
  1322.             'EUR' => '€',
  1323.             'USD' => '$',
  1324.             'TRY''TL' => '₺',
  1325.             'GBP' => '£',
  1326.             default => $currencyCode
  1327.         };
  1328.         // Verileri İşle ve Ham Haliyle Hazırla
  1329.         $processedRows = [];
  1330.         $stockRepo $this->entityManager->getRepository(\App\Entity\Stock::class);
  1331.         foreach ($dbResults as $row) {
  1332.             $currentStock $stockMap[$row['id']] ?? 0;
  1333.             // --- UNIT PRICE HESAPLAMA (Son 6 Girişin Ortalaması) ---
  1334.             // N+1 olacak ama şimdilik en güvenilir yöntem.
  1335.             // İleride performans sorunu olursa Native SQL ile optimize edilebilir.
  1336.             $lastStocks $stockRepo->findBy(
  1337.                 ['product' => $row['id']],
  1338.                 ['buyingDate' => 'DESC'],
  1339.                 6
  1340.             );
  1341.             $averageUnitPrice 0;
  1342.             if (count($lastStocks) > 0) {
  1343.                 $totalPriceSum 0;
  1344.                 foreach ($lastStocks as $stockItem) {
  1345.                     $totalPriceSum += $stockItem->getTotalUnitPrice();
  1346.                 }
  1347.                 $averageUnitPrice $totalPriceSum count($lastStocks);
  1348.             }
  1349.             $row['unitPrice'] = $averageUnitPrice;
  1350.             // -------------------------------------------------------
  1351.             // Mevsimsellik Analizi Verileri
  1352.             $monthlySalesVars = [];
  1353.             for ($k 1$k <= 12$k++) {
  1354.                 $monthlySalesVars[$k] = (float) $row['m' $k];
  1355.             }
  1356.             // Stok Ömrü Hesaplama
  1357.             $stockDurationDays 0;
  1358.             if ($currentStock <= 0) {
  1359.                 $stockDurationDays = -1// Tükendi
  1360.             } else {
  1361.                 $totalAnnualSales array_sum($monthlySalesVars);
  1362.                 if ($totalAnnualSales <= 0) {
  1363.                     $stockDurationDays 99999// Sonsuz
  1364.                 } else {
  1365.                     $tempStock $currentStock;
  1366.                     $daysAccumulated 0;
  1367.                     $calcMonthIndex = (int) date('n');
  1368.                     $calcCurrentDay = (int) date('j');
  1369.                     $safetyBreak 730;
  1370.                     while ($tempStock && $daysAccumulated $safetyBreak) {
  1371.                         $monthlySales $monthlySalesVars[$calcMonthIndex];
  1372.                         $daysInMonth 30;
  1373.                         if ($daysAccumulated == 0) {
  1374.                             $daysRemaining max(0$daysInMonth $calcCurrentDay);
  1375.                         } else {
  1376.                             $daysRemaining $daysInMonth;
  1377.                         }
  1378.                         $dailyRate $monthlySales $daysInMonth;
  1379.                         $needed $dailyRate $daysRemaining;
  1380.                         if ($needed <= 0) {
  1381.                             $daysAccumulated += $daysRemaining;
  1382.                         } else {
  1383.                             if ($tempStock >= $needed) {
  1384.                                 $tempStock -= $needed;
  1385.                                 $daysAccumulated += $daysRemaining;
  1386.                             } else {
  1387.                                 $daysLasted = ($dailyRate 0) ? ($tempStock $dailyRate) : 0;
  1388.                                 $daysAccumulated += $daysLasted;
  1389.                                 $tempStock 0;
  1390.                                 break;
  1391.                             }
  1392.                         }
  1393.                         $calcMonthIndex++;
  1394.                         if ($calcMonthIndex 12)
  1395.                             $calcMonthIndex 1;
  1396.                     }
  1397.                     $stockDurationDays $daysAccumulated;
  1398.                 }
  1399.             }
  1400.             $processedRows[] = [
  1401.                 'dbRow' => $row,
  1402.                 'currentStock' => $currentStock,
  1403.                 'stockDurationDays' => $stockDurationDays,
  1404.                 'indices' => $seasonalityService->calculateSeasonalityIndices($monthlySalesVars),
  1405.                 'monthlySalesVars' => $monthlySalesVars
  1406.             ];
  1407.         }
  1408.         // Client-side DataTable - PHP tarafında sıralama veya pagination yok
  1409.         // Tüm veri işlenip döndürülüyor
  1410.         $data = [];
  1411.         $currentMonth = (int) date('n');
  1412.         foreach ($processedRows as $pRow) {
  1413.             $row $pRow['dbRow'];
  1414.             $currentStock $pRow['currentStock'];
  1415.             $stockDurationDays $pRow['stockDurationDays'];
  1416.             $indices $pRow['indices'];
  1417.             $monthlySalesVars $pRow['monthlySalesVars'];
  1418.             $unitSymbol $row['unitSymbol'] ?? '';
  1419.             // Client-side sorting için orthogonal data formatı: { display: HTML, sort: numeric }
  1420.             $item = [
  1421.                 'name' => [
  1422.                     'display' => '<strong>' htmlspecialchars($row['name']) . '</strong><br><small class="text-muted">' htmlspecialchars($row['code']) . '</small>',
  1423.                     'sort' => $row['name']
  1424.                 ],
  1425.             ];
  1426.             $item['currentStock'] = [
  1427.                 'display' => '<span class="badge badge-info" style="font-size: 0.9rem;">' number_format($currentStock0',''.') . ' ' $unitSymbol '</span>',
  1428.                 'sort' => (int) $currentStock
  1429.             ];
  1430.             // Stok Süresi Görselleştirme
  1431.             $stockDurationHtml '';
  1432.             $stockStatus 'success'// default: yeşil (iyi durum)
  1433.             if ($stockDurationDays == -1) {
  1434.                 $stockDurationHtml '<span class="badge badge-secondary" style="background-color: #333;">Tükendi</span>';
  1435.                 $stockStatus 'empty';
  1436.             } elseif ($stockDurationDays == 99999) {
  1437.                 $stockDurationHtml '<span class="badge badge-secondary">∞ (Satış Yok)</span>';
  1438.                 $stockStatus 'noSales';
  1439.             } else {
  1440.                 $badgeClass 'badge-success'// > 90 gün
  1441.                 if ($stockDurationDays 45) { // < 45 gün KIRMIZI
  1442.                     $badgeClass 'badge-danger';
  1443.                     $stockStatus 'danger';
  1444.                 } elseif ($stockDurationDays 90) { // 45-90 gün SARI
  1445.                     $badgeClass 'badge-warning';
  1446.                     $stockStatus 'warning';
  1447.                 }
  1448.                 $displayDays $stockDurationDays >= 730 '> 2 Yıl' number_format($stockDurationDays0) . ' Gün';
  1449.                 $stockDurationHtml '<span class="badge ' $badgeClass '">' $displayDays '</span>';
  1450.             }
  1451.             // Sıralama için mantıklı değerler:
  1452.             // Tükendi (-1) → 0 (en kritik, en üstte olmalı)
  1453.             // Normal değerler → gerçek gün sayısı
  1454.             // Satış Yok (99999) → 999999 (sonsuz, en altta olmalı)
  1455.             $sortValue $stockDurationDays;
  1456.             if ($stockDurationDays == -1) {
  1457.                 $sortValue 0// Tükendi = en kritik
  1458.             } elseif ($stockDurationDays == 99999) {
  1459.                 $sortValue 999999// Satış yok = pratik olarak sonsuz
  1460.             }
  1461.             $item['stockDuration'] = [
  1462.                 'display' => $stockDurationHtml,
  1463.                 'sort' => $sortValue,
  1464.                 'status' => $stockStatus // Filtreleme için: empty, noSales, danger, warning, success
  1465.             ];
  1466.             // Filtreleme için ek metadata
  1467.             $item['_meta'] = [
  1468.                 'stockStatus' => $stockStatus,
  1469.                 'stockValue' => (int) $currentStock,
  1470.                 'productName' => $row['name'],
  1471.                 'productCode' => $row['code']
  1472.             ];
  1473.             // Ayları ekle
  1474.             for ($i 1$i <= 12$i++) {
  1475.                 $val $monthlySalesVars[$i];
  1476.                 $idx $indices[$i];
  1477.                 $style '';
  1478.                 if ($i $currentMonth) {
  1479.                     $style 'background-color: #ffebee; color: #c62828;';
  1480.                 } elseif ($i === $currentMonth) {
  1481.                     $style 'background-color: #e8f5e9; color: #2e7d32;';
  1482.                 }
  1483.                 if ($val 0) {
  1484.                     $formattedVal number_format($val0',''.');
  1485.                     $formattedIdx number_format($idx2);
  1486.                     $content $formattedVal ' <small style="font-size: 0.7em; opacity: 0.8;">/ ' $formattedIdx 'x</small>';
  1487.                 } else {
  1488.                     $content '<span style="opacity: 0.5;">-</span>';
  1489.                 }
  1490.                 if ($style !== '') {
  1491.                     $displayContent '<div style="' $style ' border-radius: 5px; padding: 6px 0;">' $content '</div>';
  1492.                 } else {
  1493.                     $displayContent $content;
  1494.                 }
  1495.                 $item['m' $i] = [
  1496.                     'display' => $displayContent,
  1497.                     'sort' => (float) $val
  1498.                 ];
  1499.             }
  1500.             // Toplam
  1501.             $total = (float) $row['total'];
  1502.             $item['total'] = [
  1503.                 'display' => '<span class="font-weight-bold text-primary">' number_format($total0',''.') . '</span>',
  1504.                 'sort' => $total
  1505.             ];
  1506.             // --- TAVSİYE EDİLEN STOK ALIMI ve MALİYETİ ---
  1507.             // --- TAVSİYE EDİLEN STOK ALIMI ve MALİYETİ (Merkezi Mantık) ---
  1508.             $unitPrice = (float) $row['unitPrice'];
  1509.             // Mevcut ay ve sonraki 2 ayın taleplerini al (Döngüsel)
  1510.             $getDemand = function ($m) use ($monthlySalesVars) {
  1511.                 $idx = (($m 1) % 12) + 1;
  1512.                 return $monthlySalesVars[$idx] ?? 0;
  1513.             };
  1514.             $d1 $getDemand($currentMonth);
  1515.             $d2 $getDemand($currentMonth 1);
  1516.             $d3 $getDemand($currentMonth 2);
  1517.             // Merkezi Servis ile Analiz
  1518.             $stockAnalysis $forecastingService->checkStockStatus((float) $currentStock$d1$d2$d3$unitPrice);
  1519.             $suggestedOrderQty $stockAnalysis['order_qty'];
  1520.             $suggestedOrderCost $stockAnalysis['order_cost'];
  1521.             $coverageDateStr '';
  1522.             if ($suggestedOrderQty 0) {
  1523.                 // Tooltip için tarih hesapla: Bugün + 3 ay
  1524.                 $targetDate = new \DateTime();
  1525.                 $targetDate->modify('+3 months');
  1526.                 $coverageDateStr $targetDate->format('d.m.Y');
  1527.             }
  1528.             // 1. Tavsiye Edilen Stok Alımı (Tooltip ile)
  1529.             $tooltipAttr $suggestedOrderQty 'data-toggle="tooltip" title="' $coverageDateStr ' tarihine kadar yeterli"' '';
  1530.             $style $suggestedOrderQty 'color: #e74a3b; font-size:1.1em; cursor:help;' 'text-muted';
  1531.             $valInfo $suggestedOrderQty number_format($suggestedOrderQty0',''.') : '-';
  1532.             $item['suggestedOrderQty'] = [
  1533.                 'display' => '<span class="font-weight-bold" ' $tooltipAttr ' style="' $style '">' $valInfo '</span>',
  1534.                 'sort' => $suggestedOrderQty
  1535.             ];
  1536.             // 2. Tavsiye Edilen Stok Maliyeti (Yeni Sütun)
  1537.             $item['suggestedOrderCost'] = [
  1538.                 'display' => $suggestedOrderCost '<span class="text-danger">' number_format($suggestedOrderCost2',''.') . ' ' $currencySymbol '</span>' '-',
  1539.                 'sort' => $suggestedOrderCost
  1540.             ];
  1541.             // 3. Birim Maliyet (Yeni Sütun)
  1542.             $item['unitPrice'] = [
  1543.                 'display' => number_format($unitPrice2',''.') . ' ' $currencySymbol,
  1544.                 'sort' => $unitPrice
  1545.             ];
  1546.             $data[] = $item;
  1547.         }
  1548.         // Client-side DataTable için sadece data array döndürülüyor
  1549.         return new JsonResponse([
  1550.             'data' => $data
  1551.         ]);
  1552.     }
  1553.     /**
  1554.      * AJAX endpoint for monthly stock purchase detail (used in budget planning info popup)
  1555.      */
  1556.     #[Route('/admin/dashboard/stock-purchase-detail/{month}'name'admin_dashboard_stock_purchase_detail'methods: ['GET'])]
  1557.     public function getStockPurchaseDetail(int $monthFinancialForecastingService $financialService): JsonResponse
  1558.     {
  1559.         // Validate month
  1560.         if ($month || $month 12) {
  1561.             return new JsonResponse(['error' => 'Invalid month'], 400);
  1562.         }
  1563.         $detail $financialService->getMonthlyStockPurchaseDetail(20252026$monthtrue);
  1564.         return new JsonResponse($detail);
  1565.     }
  1566.     /**
  1567.      * AJAX endpoint for 2026 Stock & Order Simulation data (for DataTables on Dashboard)
  1568.      */
  1569.     #[Route('/admin/api/stock-simulation-data'name'app_dashboard_stock_simulation_data'methods: ['GET'])]
  1570.     public function getStockSimulationData(\App\Service\Analysis\StrategicForecastingService $forecastingService): JsonResponse
  1571.     {
  1572.         // Generate forecast data
  1573.         $forecastData $forecastingService->generateForecast(20252026);
  1574.         $forecasts $forecastData['product_forecasts'];
  1575.         $data = [];
  1576.         foreach ($forecasts as $pid => $product) {
  1577.             $row = [
  1578.                 'product_code' => $product['code'],
  1579.                 'product_name' => $product['name'],
  1580.             ];
  1581.             // Add monthly data
  1582.             $totalSales2025 0;
  1583.             for ($month 1$month <= 12$month++) {
  1584.                 $plan $product['plan'][$month] ?? [];
  1585.                 $demand = (int) ($plan['demand'] ?? 0);
  1586.                 $row['month_' $month '_stock'] = (int) ($plan['stock_end'] ?? 0);
  1587.                 $row['month_' $month '_demand'] = $demand;
  1588.                 $row['month_' $month '_order'] = (int) ($plan['order_qty'] ?? 0);
  1589.                 $row['month_' $month '_status'] = $plan['status'] ?? 'secure';
  1590.                 $totalSales2025 += $demand;
  1591.             }
  1592.             $row['total_sales_2025'] = $totalSales2025;
  1593.             $data[] = $row;
  1594.         }
  1595.         return new JsonResponse(['data' => $data]);
  1596.     }
  1597.     /**
  1598.      * AJAX endpoint for Yearly Plan Table data
  1599.      * Veri Kaynakları:
  1600.      * - Yıllık Satılan: product_monthly_stats.total_sales_qty (SUM for year)
  1601.      * - Mevcut Stok: stock_info.total_quantity (SUM)
  1602.      * - Birim Fiyat: product_monthly_stats.avg_cost_price (Ağırlıklı ortalama)
  1603.      */
  1604.     #[Route('/admin/api/yearly-plan-data'name'app_dashboard_yearly_plan_data'methods: ['GET'])]
  1605.     public function getYearlyPlanData(ProductMonthlyStatsRepository $statsRepository): JsonResponse
  1606.     {
  1607.         // 2025 yılı için tüm ürünlerin aylık istatistiklerini al
  1608.         $allStats $statsRepository->findBy(['year' => 2025]);
  1609.         // Ürün bazlı verileri topla
  1610.         $productData = [];
  1611.         foreach ($allStats as $stat) {
  1612.             $product $stat->getProduct();
  1613.             if ($product === null) {
  1614.                 continue;
  1615.             }
  1616.             $pId $product->getId();
  1617.             if (!isset($productData[$pId])) {
  1618.                 // Mevcut stok: StockInfo tablosundan
  1619.                 $currentStock 0;
  1620.                 foreach ($product->getStockInfos() as $stockInfo) {
  1621.                     $currentStock += $stockInfo->getTotalQuantity();
  1622.                 }
  1623.                 $productData[$pId] = [
  1624.                     'id' => $pId,
  1625.                     'name' => $product->getName(),
  1626.                     'code' => $product->getCode(),
  1627.                     'currentStock' => (float) $currentStock,
  1628.                     'totalSales' => 0,
  1629.                     'totalCostSum' => 0,
  1630.                     'totalQtyForCost' => 0,
  1631.                 ];
  1632.             }
  1633.             $m $stat->getMonth();
  1634.             if ($m >= && $m <= 12) {
  1635.                 $qty = (float) $stat->getTotalSalesQty();
  1636.                 $avgCostPrice = (float) $stat->getAvgCostPrice();
  1637.                 $productData[$pId]['totalSales'] += $qty;
  1638.                 // Ağırlıklı ortalama maliyet hesabı için
  1639.                 if ($qty && $avgCostPrice 0) {
  1640.                     $productData[$pId]['totalCostSum'] += ($qty $avgCostPrice);
  1641.                     $productData[$pId]['totalQtyForCost'] += $qty;
  1642.                 }
  1643.             }
  1644.         }
  1645.         // Hesaplamaları yap ve sonuç dizisini oluştur
  1646.         $result = [];
  1647.         foreach ($productData as $pId => $data) {
  1648.             $yearlySales $data['totalSales'];
  1649.             $currentStock $data['currentStock'];
  1650.             // Birim Fiyat: Ağırlıklı ortalama
  1651.             $unitPrice 0;
  1652.             if ($data['totalQtyForCost'] > 0) {
  1653.                 $unitPrice $data['totalCostSum'] / $data['totalQtyForCost'];
  1654.             }
  1655.             // Hesaplamalar
  1656.             $salesMinusStock $yearlySales $currentStock;
  1657.             $orderQty max(0ceil($salesMinusStock));
  1658.             $totalCost $orderQty $unitPrice;
  1659.             $result[] = [
  1660.                 'id' => $pId,
  1661.                 'name' => $data['name'],
  1662.                 'code' => $data['code'],
  1663.                 'yearlySales' => ceil($yearlySales),        // Yıllık Satılan
  1664.                 'currentStock' => $currentStock,             // Mevcut Stok
  1665.                 'salesMinusStock' => ceil($salesMinusStock), // Satılan - Stok
  1666.                 'orderQty' => $orderQty,                     // Sipariş Miktarı
  1667.                 'unitPrice' => round($unitPrice2),         // Birim Fiyat
  1668.                 'totalCost' => round($totalCost2),         // Toplam
  1669.             ];
  1670.         }
  1671.         // Yıllık satışa göre sırala (çoktan aza)
  1672.         usort($result, function ($a$b) {
  1673.             return $b['yearlySales'] <=> $a['yearlySales'];
  1674.         });
  1675.         return new JsonResponse(['data' => $result]);
  1676.     }
  1677.     /**
  1678.      * AJAX endpoint for Monthly Plan Table data
  1679.      * Seçilen ay için ürün bazlı satış ve sipariş planı
  1680.      * Yıllık satışa göre sıralanır (en çoktan en aza)
  1681.      */
  1682.     #[Route('/admin/api/monthly-plan-data/{month}'name'app_dashboard_monthly_plan_data'methods: ['GET'])]
  1683.     public function getMonthlyPlanData(int $monthProductMonthlyStatsRepository $statsRepository): JsonResponse
  1684.     {
  1685.         // Validate month
  1686.         if ($month || $month 12) {
  1687.             return new JsonResponse(['error' => 'Invalid month'], 400);
  1688.         }
  1689.         // 2025 yılı için tüm ürünlerin aylık istatistiklerini al
  1690.         $allStats $statsRepository->findBy(['year' => 2025]);
  1691.         // Ürün bazlı verileri topla
  1692.         $productData = [];
  1693.         foreach ($allStats as $stat) {
  1694.             $product $stat->getProduct();
  1695.             if ($product === null) {
  1696.                 continue;
  1697.             }
  1698.             $pId $product->getId();
  1699.             if (!isset($productData[$pId])) {
  1700.                 // Mevcut stok: StockInfo tablosundan
  1701.                 $currentStock 0;
  1702.                 foreach ($product->getStockInfos() as $stockInfo) {
  1703.                     $currentStock += $stockInfo->getTotalQuantity();
  1704.                 }
  1705.                 $productData[$pId] = [
  1706.                     'id' => $pId,
  1707.                     'name' => $product->getName(),
  1708.                     'code' => $product->getCode(),
  1709.                     'currentStock' => (float) $currentStock,
  1710.                     'yearlySales' => 0,
  1711.                     'monthlySales' => 0,
  1712.                     'monthlyUnitPrice' => 0,
  1713.                 ];
  1714.             }
  1715.             $m $stat->getMonth();
  1716.             $qty = (float) $stat->getTotalSalesQty();
  1717.             $avgCostPrice = (float) $stat->getAvgCostPrice();
  1718.             // Yıllık toplam satış
  1719.             if ($m >= && $m <= 12) {
  1720.                 $productData[$pId]['yearlySales'] += $qty;
  1721.             }
  1722.             // Seçilen ay için satış
  1723.             if ($m === $month) {
  1724.                 $productData[$pId]['monthlySales'] = $qty;
  1725.                 $productData[$pId]['monthlyUnitPrice'] = $avgCostPrice;
  1726.             }
  1727.         }
  1728.         // Hesaplamaları yap ve sonuç dizisini oluştur
  1729.         $result = [];
  1730.         foreach ($productData as $pId => $data) {
  1731.             $monthlySales $data['monthlySales'];
  1732.             $currentStock $data['currentStock'];
  1733.             $unitPrice $data['monthlyUnitPrice'];
  1734.             // Hesaplamalar
  1735.             $stockMinusSales $currentStock $monthlySales;
  1736.             $orderQty max(0ceil($monthlySales $currentStock));
  1737.             $totalCost $orderQty $unitPrice;
  1738.             $result[] = [
  1739.                 'id' => $pId,
  1740.                 'name' => $data['name'],
  1741.                 'code' => $data['code'],
  1742.                 'currentStock' => $currentStock,             // Mevcut Stok
  1743.                 'monthlySales' => ceil($monthlySales),       // Aylık Satılan
  1744.                 'stockMinusSales' => ceil($stockMinusSales), // Stok - Satılan
  1745.                 'unitPrice' => round($unitPrice2),         // Birim Fiyat
  1746.                 'orderQty' => $orderQty,                     // Sipariş Miktarı
  1747.                 'totalCost' => round($totalCost2),         // Toplam
  1748.                 'yearlySales' => ceil($data['yearlySales']), // Sıralama için
  1749.             ];
  1750.         }
  1751.         // Yıllık satışa göre sırala (çoktan aza)
  1752.         usort($result, function ($a$b) {
  1753.             return $b['yearlySales'] <=> $a['yearlySales'];
  1754.         });
  1755.         return new JsonResponse(['data' => $result]);
  1756.     }
  1757.     #[Route('/monthly-plan/{month}'name'app_dashboard_monthly_plan_data'methods: ['GET'])]
  1758.     public function getMonthlyOrderPlan(int $monthProductMonthlyStatsRepository $statsRepository\Symfony\Component\HttpFoundation\Request $request): JsonResponse
  1759.     {
  1760.         $filterName $request->query->get('name');
  1761.         $filterCodes $request->query->all()['codes'] ?? [];
  1762.         $filterMeasurements $request->query->all()['measurements'] ?? [];
  1763.         $filterUnits $request->query->all()['units'] ?? [];
  1764.         // 1. Fetch all 2025 stats
  1765.         $allStats2025 $statsRepository->findBy(['year' => 2025]);
  1766.         // 2. Group by Product
  1767.         $productData = [];
  1768.         foreach ($allStats2025 as $stat) {
  1769.             $product $stat->getProduct();
  1770.             // Skip if product is null OR if it has no measurement unit
  1771.             if ($product === null || $product->getMeasurementUnit() === null) {
  1772.                 continue;
  1773.             }
  1774.             $code $product->getCode();
  1775.             $key $code strtoupper(trim($code)) : $product->getId();
  1776.             if (!isset($productData[$key])) {
  1777.                 $productData[$key] = [
  1778.                     'name' => $product->getName(),
  1779.                     'code' => $product->getCode(),
  1780.                     'currentStock' => 0,
  1781.                     'total2025' => 0,
  1782.                     'monthlySales' => 0// Sales for the requested month
  1783.                     'totalCostSum' => 0,
  1784.                     'totalQtyForCost' => 0,
  1785.                     'seen_pids' => [],
  1786.                     'measurement' => $product->getMeasurement() ? $product->getMeasurement()->getMeasurement() : '',
  1787.                     'measurementId' => $product->getMeasurement() ? $product->getMeasurement()->getId() : null,
  1788.                     'unitId' => $product->getMeasurementUnit() ? $product->getMeasurementUnit()->getId() : null,
  1789.                     'unitSymbol' => $product->getMeasurementUnit() ? $product->getMeasurementUnit()->getSymbol() : ''
  1790.                 ];
  1791.             }
  1792.             $pId $product->getId();
  1793.             if (!in_array($pId$productData[$key]['seen_pids'])) {
  1794.                 $thisStock array_reduce($product->getStockInfos()->toArray(), function ($sum$item) {
  1795.                     return $sum $item->getTotalQuantity();
  1796.                 }, 0);
  1797.                 $productData[$key]['currentStock'] += $thisStock;
  1798.                 $productData[$key]['seen_pids'][] = $pId;
  1799.             }
  1800.             $m $stat->getMonth();
  1801.             $qty = (float) $stat->getTotalSalesQty();
  1802.             $avgCost = (float) $stat->getAvgCostPrice();
  1803.             // Total 2025 Sales
  1804.             if ($m >= && $m <= 12) {
  1805.                 $productData[$key]['total2025'] += $qty;
  1806.                 // For Unit Cost Calculation
  1807.                 if ($qty && $avgCost 0) {
  1808.                     $productData[$key]['totalCostSum'] += ($qty $avgCost);
  1809.                     $productData[$key]['totalQtyForCost'] += $qty;
  1810.                 }
  1811.             }
  1812.             // Month specific sales
  1813.             if ($m == $month) {
  1814.                 $productData[$key]['monthlySales'] += $qty;
  1815.             }
  1816.         }
  1817.         // 3. Format result and Calculate Totals
  1818.         $result = [];
  1819.         $totalS1Order 0;
  1820.         $totalS1Cost 0;
  1821.         $totalS2Order 0;
  1822.         $totalS2Cost 0;
  1823.         foreach ($productData as $key => $data) {
  1824.             // Server-side filtering
  1825.             if ($filterName && stripos($data['name'], $filterName) === false)
  1826.                 continue;
  1827.             if (!empty($filterCodes) && !in_array($data['code'], $filterCodes))
  1828.                 continue;
  1829.             if (!empty($filterMeasurements) && !in_array($data['measurementId'], $filterMeasurements))
  1830.                 continue;
  1831.             if (!empty($filterUnits) && !in_array($data['unitId'], $filterUnits))
  1832.                 continue;
  1833.             // Unit Price (Weighted Average)
  1834.             $unitPrice 0;
  1835.             if ($data['totalQtyForCost'] > 0) {
  1836.                 $unitPrice $data['totalCostSum'] / $data['totalQtyForCost'];
  1837.             }
  1838.             $currentStock $data['currentStock'];
  1839.             $monthSales ceil($data['monthlySales']);
  1840.             $total2025 ceil($data['total2025']);
  1841.             // Scenario 1: Equal Distribution (Stock / 12)
  1842.             $allocatedEqual $currentStock 12;
  1843.             $orderEqual 0;
  1844.             // Order amount is difference between Demand (monthSales) and Allocated Stock
  1845.             // User requested: "2025 yılında o ay satılan gerçek stok miktarından her iki senaryodaki miktarları çıkaracak ortaya çıkan miktar iki senaryo için sipariş miktarı olacak."
  1846.             // So: monthSales - allocatedEqual
  1847.             if ($monthSales $allocatedEqual) {
  1848.                 $orderEqual $monthSales $allocatedEqual;
  1849.             }
  1850.             $costEqual $orderEqual $unitPrice;
  1851.             // Scenario 2: Weighted Distribution (Stock * (MonthSale / YearSale))
  1852.             $allocatedWeighted 0;
  1853.             if ($total2025 0) {
  1854.                 $ratio $monthSales $total2025;
  1855.                 $allocatedWeighted $currentStock $ratio;
  1856.             }
  1857.             $orderWeighted 0;
  1858.             if ($monthSales $allocatedWeighted) {
  1859.                 $orderWeighted $monthSales $allocatedWeighted;
  1860.             }
  1861.             $costWeighted $orderWeighted $unitPrice;
  1862.             // Add to totals
  1863.             $totalS1Order += ceil($orderEqual);
  1864.             $totalS1Cost += $costEqual;
  1865.             $totalS2Order += ceil($orderWeighted);
  1866.             $totalS2Cost += $costWeighted;
  1867.             $result[] = [
  1868.                 'name' => $data['name'],
  1869.                 'code' => $data['code'],
  1870.                 'currentStock' => $currentStock,
  1871.                 'sales2025' => $total2025,
  1872.                 'monthSales' => $monthSales// Demand
  1873.                 'unitPrice' => round($unitPrice2),
  1874.                 // Scenario 1
  1875.                 'allocatedEqual' => floor($allocatedEqual),
  1876.                 'orderEqual' => ceil($orderEqual),
  1877.                 'costEqual' => round($costEqual2),
  1878.                 // Scenario 2
  1879.                 'allocatedWeighted' => floor($allocatedWeighted),
  1880.                 'orderWeighted' => ceil($orderWeighted),
  1881.                 'costWeighted' => round($costWeighted2),
  1882.                 'measurement' => $data['measurement'],
  1883.                 'measurementId' => $data['measurementId'],
  1884.                 'unitId' => $data['unitId'],
  1885.                 'unitSymbol' => $data['unitSymbol']
  1886.             ];
  1887.         }
  1888.         // Sort by Monthly Sales Descending (Primary focus) or Order Cost
  1889.         usort($result, function ($a$b) {
  1890.             return $b['monthSales'] <=> $a['monthSales'];
  1891.         });
  1892.         return new JsonResponse([
  1893.             'data' => $result,
  1894.             'totals' => [
  1895.                 's1Order' => $totalS1Order,
  1896.                 's1Cost' => round($totalS1Cost2),
  1897.                 's2Order' => $totalS2Order,
  1898.                 's2Cost' => round($totalS2Cost2)
  1899.             ]
  1900.         ]);
  1901.     }
  1902.     /**
  1903.      * AJAX endpoint for detailed sales view (Sleeping Stock)
  1904.      * Combines detailed system sales with legacy aggregate data
  1905.      */
  1906.     #[Route('/admin/dashboard/product-sales-details/{id}'name'admin_dashboard_product_sales_details'methods: ['GET'])]
  1907.     public function getProductSalesDetails(int $idProductRepository $productRepositorySalesRepository $salesRepositoryProductMonthlyStatsRepository $statsRepository): JsonResponse
  1908.     {
  1909.         $product $productRepository->find($id);
  1910.         if (!$product) {
  1911.             return new JsonResponse(['error' => 'Product not found'], 404);
  1912.         }
  1913.         // 1. Fetch System Sales (Detailed)
  1914.         $sales $salesRepository->createQueryBuilder('s')
  1915.             ->join('s.productsSolds''ps')
  1916.             ->where('ps.product = :product')
  1917.             ->andWhere('s.deleted = :deleted')
  1918.             ->setParameter('product'$product)
  1919.             ->setParameter('deleted'false)
  1920.             ->orderBy('s.salesDate''DESC')
  1921.             ->getQuery()
  1922.             ->getResult();
  1923.         $detailedSales = [];
  1924.         $salesByMonth = []; // Key: 'Y-m', Value: quantity sum
  1925.         /** @var Sales $sale */
  1926.         foreach ($sales as $sale) {
  1927.             foreach ($sale->getProductsSolds() as $ps) {
  1928.                 if ($ps->getProduct()->getId() === $id) {
  1929.                     $date $sale->getSalesDate();
  1930.                     $monthKey $date $date->format('Y-m') : 'Unknown';
  1931.                     if (!isset($salesByMonth[$monthKey])) {
  1932.                         $salesByMonth[$monthKey] = 0;
  1933.                     }
  1934.                     $salesByMonth[$monthKey] += $ps->getQuantity();
  1935.                     $customerName $sale->getCustomer() ? $sale->getCustomer()->getFullName() : 'Unknown';
  1936.                     // Check if customer name might be hidden/empty
  1937.                     if (trim($customerName) === '')
  1938.                         $customerName 'Unknown';
  1939.                     $detailedSales[] = [
  1940.                         'type' => 'system',
  1941.                         'date' => $date $date->format('d.m.Y') : '-',
  1942.                         'customer' => $customerName,
  1943.                         'quantity' => (float) $ps->getQuantity(),
  1944.                         'price' => number_format((float) $ps->getTotalUnitPrice(), 2),
  1945.                         'total' => number_format((float) $ps->getTotalPrice(), 2),
  1946.                         'currency' => 'EUR'// Defaulting to EUR as per potential settings
  1947.                         'invoice' => $sale->getInvoiceNumber() ?: '-'
  1948.                     ];
  1949.                 }
  1950.             }
  1951.         }
  1952.         // 2. Fetch Monthly Stats (Aggregate) to find gaps
  1953.         // We look at the last 2 years for sleeping stock analysis usually
  1954.         $stats $statsRepository->findBy(['product' => $product], ['year' => 'DESC''month' => 'DESC']);
  1955.         $finalList $detailedSales;
  1956.         // Iterate stats to find "missing" sales (Legacy/Manual)
  1957.         foreach ($stats as $stat) {
  1958.             $year $stat->getYear();
  1959.             $month $stat->getMonth();
  1960.             $monthKey sprintf('%04d-%02d'$year$month);
  1961.             $statQty = (float) $stat->getTotalSalesQty();
  1962.             $systemQty $salesByMonth[$monthKey] ?? 0;
  1963.             // If aggregate is significantly higher than system sales, show the difference as legacy
  1964.             if ($statQty $systemQty 0.01) {
  1965.                 $diff $statQty $systemQty;
  1966.                 // Add a "Legacy/Aggregate" row
  1967.                 $finalList[] = [
  1968.                     'type' => 'legacy',
  1969.                     'date' => sprintf('%02d.%04d'$month$year), // Monthly precision
  1970.                     'customer' => '<em>Geçmiş Satış Kaydı (Detaysız)</em>',
  1971.                     'quantity' => $diff,
  1972.                     'price' => number_format((float) $stat->getAvgSalesPrice(), 2),
  1973.                     'total' => number_format($diff * (float) $stat->getAvgSalesPrice(), 2),
  1974.                     'currency' => 'EUR',
  1975.                     'invoice' => '-'
  1976.                 ];
  1977.             }
  1978.         }
  1979.         // Sort by date (parsing might be tricky mixed formats, but we can standardize sort key)
  1980.         usort($finalList, function ($a$b) {
  1981.             // Helper to get timestamp
  1982.             $getTime = function ($item) {
  1983.                 if ($item['type'] === 'system') {
  1984.                     $d \DateTime::createFromFormat('d.m.Y'$item['date']);
  1985.                     return $d $d->getTimestamp() : 0;
  1986.                 } else {
  1987.                     // Legacy date is 'm.Y', treat as start of month
  1988.                     $d \DateTime::createFromFormat('m.Y'$item['date']);
  1989.                     return $d $d->getTimestamp() : 0;
  1990.                 }
  1991.             };
  1992.             return $getTime($b) <=> $getTime($a); // DESC
  1993.         });
  1994.         return new JsonResponse([
  1995.             'product' => [
  1996.                 'name' => $product->getName(),
  1997.                 'code' => $product->getCode()
  1998.             ],
  1999.             'sales' => $finalList
  2000.         ]);
  2001.     }
  2002. }