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

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