<?php
namespace App\Controller\Admin\Dashboard;
use App\Dto\SellerDetailRequestDto;
use App\Entity\Product;
use App\Enum\ProductTypeEnum;
use App\Entity\ProductsSold;
use App\Entity\Sales;
use App\Entity\StockInfo;
use App\Entity\User;
use App\Entity\Warehouse;
use App\Repository\ProductRepository;
use App\Repository\StockRepository;
use App\Repository\ProductMonthlyStatsRepository;
use App\Service\Analysis\FinancialForecastingService;
use App\Services\Product\Impl\ProductServiceImpl;
use App\Services\Sales\Impl\SalesProductsServiceImpl;
use App\Services\Sales\Impl\SalesServiceImpl;
use App\Services\Sales\SalesService;
use App\Services\Stock\StockService;
use App\Services\Stock\StockTransferService;
use App\Services\User\UserService;
use App\Services\Warehouse\Impl\WarehouseServiceImpl;
use App\Utils\CurrencyHelper;
use App\Utils\DateUtil;
use App\Utils\Enums\DateZoneEnums;
use App\Utils\Referer;
use DateInterval;
use DatePeriod;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Constraints\DateTime;
use function Doctrine\ORM\QueryBuilder;
class DashboardController extends AbstractController
{
use Referer;
private UserService $userService;
private StockService $stockService;
private SalesService $salesService;
private StockTransferService $stockTransferService;
private EntityManagerInterface $entityManager;
private StockRepository $stockRepository;
private SalesServiceImpl $salesServiceImpl;
private ProductServiceImpl $productServiceImpl;
private PaginatorInterface $paginator;
private WarehouseServiceImpl $warehouseService;
private SalesProductsServiceImpl $salesProductsServiceImpl;
/**
* @param UserService $userService
* @param StockService $stockService
* @param SalesService $salesService
* @param StockTransferService $stockTransferService
* @param EntityManagerInterface $entityManager
* @param StockRepository $stockRepository
* @param SalesServiceImpl $salesServiceImpl
* @param ProductServiceImpl $productServiceImpl
* @param PaginatorInterface $paginator
* @param WarehouseServiceImpl $warehouseService
* @param SalesProductsServiceImpl $salesProductsServiceImpl
*/
public function __construct(UserService $userService, StockService $stockService, SalesService $salesService, StockTransferService $stockTransferService, EntityManagerInterface $entityManager, StockRepository $stockRepository, SalesServiceImpl $salesServiceImpl, ProductServiceImpl $productServiceImpl, PaginatorInterface $paginator, WarehouseServiceImpl $warehouseService, SalesProductsServiceImpl $salesProductsServiceImpl)
{
$this->userService = $userService;
$this->stockService = $stockService;
$this->salesService = $salesService;
$this->stockTransferService = $stockTransferService;
$this->entityManager = $entityManager;
$this->stockRepository = $stockRepository;
$this->salesServiceImpl = $salesServiceImpl;
$this->productServiceImpl = $productServiceImpl;
$this->paginator = $paginator;
$this->warehouseService = $warehouseService;
$this->salesProductsServiceImpl = $salesProductsServiceImpl;
}
public function getProfitByPeriots(DateZoneEnums $dateZoneEnums, $year = null)
{
if ($year == null) {
$year = date('Y');
}
$salesRepo = $this->entityManager->getRepository(Sales::class);
if ($dateZoneEnums->value == 'monthly') {
$time = new \DateTimeImmutable('1 month ago');
$endTime = new \DateTimeImmutable('now');
}
if ($dateZoneEnums->value == 'weekly') {
$time = new \DateTimeImmutable('1 week ago');
$endTime = new \DateTimeImmutable('now');
}
if ($dateZoneEnums->value == 'yearly') {
$time = new \DateTimeImmutable('first day of January ' . $year);
$endTime = new \DateTimeImmutable('last day of December ' . $year . ' 23:59:59');
}
$qb = $salesRepo->createQueryBuilder('s')
->leftJoin('s.productsSolds', 'productsSolds')
->where('s.salesDate >= :time')
->andWhere('s.salesDate <= :endTime')
->setParameter('time', $time)
->setParameter('endTime', $endTime);
if (!$this->canCurrentUserSeeHiddenSales()) {
$qb->andWhere('s.visible = :visible')
->setParameter('visible', true);
}
$sales = $qb->getQuery()->getResult();
$totalProfit = 0.00;
foreach ($sales as $sale) {
foreach ($sale->getProductsSolds() as $productsSold) {
$totalProfit += $this->salesProductsServiceImpl->calculateProfitByProductSold($productsSold);
}
}
return CurrencyHelper::convertToCurrency($totalProfit);
}
#[Route('/get-products-summary', name: 'get_products_summary')]
public function getProductsSummary(Request $request, ProductRepository $productRepository)
{
$s = isset($request->get('search')['value']) ? $request->get('search')['value'] : null;
$startDate = $request->get('startDate') != null ? $request->get('startDate') : (new \DateTimeImmutable('now'))->modify('-1 month')->format('Y-m-d');
$endDate = $request->get('endDate') != null ? $request->get('endDate') : (new \DateTimeImmutable('now'))->format('Y-m-d');
$id = $request->get('id');
$column = $request->get('order') != null ? $request->get('order')[0]['column'] : '0';
$seller = $request->get('sellerId') == 0 ? null : $request->get('sellerId');
$dir = $request->get('order') != null ? $request->get('order')[0]['dir'] : 'asc';
$limit = $request->get('length');
if ($request->get('start'))
$page = 1 + ($request->get('start') / $limit);
else
$page = 1;
$startDate = \DateTimeImmutable::createFromFormat('Y-m-d', $startDate);
$endDate = \DateTimeImmutable::createFromFormat('Y-m-d', $endDate);
$query = $this->entityManager->getRepository(Product::class)->createQueryBuilder('p')
->leftJoin('p.productsSolds', 'productsSolds')
->leftJoin('productsSolds.sales', 'sales')
->leftJoin('sales.seller', 'seller')
->leftJoin('p.measurement', 'w')
->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')
->setParameter('s', "%$s%")
->orWhere('w.measurement LIKE :s')
->setParameter('s', "%$s%");
if ($seller != null) {
$query->andWhere('seller.id = :sellerId')
->setParameter('sellerId', $seller);
}
$query = $query->getQuery();
$products = $query->getResult();
// switch ($column){
// case 0:
// $column = 'id';
// break;
// case 1:
// $column = 'productName';
// break;
// case 2:
// $column = 'code';
// break;
// case 3:
// $column = 'soldPrice';
// break;
// case 4:
// $column = 'totalEarn';
// break;
// case 5:
// $column = 'totalProfit';
// break;
// }
$datas = $this->createArrayForProductsDatatable(
$products,
$startDate,
$endDate,
$column,
$dir,
$page,
$limit
);
// dd($datas);
//
// dd($column);
return $this->json($datas, 200);
dd($products->getItems());
return $this->json(['data' => 'ss'], 200);
$products = $this->entityManager->getRepository(Product::class);
}
public function createArrayForProductsDatatable($products, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, $column, $dir, $page, $limit)
{
$records = [];
$records["data"] = [];
/**
* @var Product $entity
*/
foreach ($products as $entity) {
$records["data"][] = array(
$entity->getId(),
$entity->getName(),
$entity->getCode() . " - " . ($entity->getMeasurement() != null ? $entity->getMeasurement()->getMeasurement() : ''),
$this->salesServiceImpl->getTotalSoldQuantityByDateSellerAndProduct($startDate, $endDate, null, $entity->getId()),
$this->salesServiceImpl->getTotalSalesPriceByDateSellerAndProduct($startDate, $endDate, null, $entity->getId()),
$this->salesServiceImpl->getTotalProfitPriceByDateSellerAndProduct($startDate, $endDate, null, $entity->getId()),
"<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>"
);
}
$this->sortArrayByKey($records["data"], $column, false, $dir == 'asc');
$pagination = $this->paginator->paginate(
$records["data"],
$page,
$limit
);
$records['recordsTotal'] = $pagination->getTotalItemCount();
$records['recordsFiltered'] = $pagination->getTotalItemCount();
$records["data"] = $pagination->getItems();
return $records;
}
#[Route('/sales-prices-and-profits-by-year-range-month', name: 'sales_prices_and_profits_by_year_range_month')]
public function getSalesPricesAndProfitsByYearRangeMonth(Request $request)
{
$year = $request->get('year') != null ? $request->get('year') : date('Y');
$months = [];
$format = 'Y-m-d';
for ($month = 1; $month <= 12; $month++) {
// if($month > date('m'))
// break;
$startOfMonth = \DateTimeImmutable::createFromFormat($format, "$year-$month-01");
$endOfMonth = (\DateTimeImmutable::createFromFormat($format, "$year-$month-01"))->modify('last day of this month');
$months[$month - 1]['start'] = $startOfMonth;
$months[$month - 1]['end'] = $endOfMonth;
}
$data = [];
foreach ($months as $month) {
$data['profit'][] = $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate($month['start'], $month['end']);
$data['sales'][] = $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate($month['start'], $month['end']);
}
// $qb = $this->entityManager->getRepository(Sales::class)->createQueryBuilder('s')
// ->select('MONTH(s.salesDate) as month, SUM(s.totalPurchasePrice) as total')
// ->where('YEAR(s.salesDate) = :year')
// ->setParameter('year', $year)
// ->groupBy('month')
// ->orderBy('month', 'ASC')
// ->getQuery();
// dd($qb->getResult());
return $this->json($data, 200);
}
function sortArrayByKey(&$array, $key, $string = false, $asc = true)
{
if ($string) {
usort($array, function ($a, $b) use (&$key, &$asc) {
if ($asc)
return strcmp(strtolower($a[$key]), strtolower($b[$key]));
else
return strcmp(strtolower($b[$key]), strtolower($a[$key]));
});
} else {
usort($array, function ($a, $b) use (&$key, &$asc) {
if ($a[$key] == $b[$key]) {
return 0;
}
if ($asc)
return ($a[$key] < $b[$key]) ? -1 : 1;
else
return ($a[$key] > $b[$key]) ? -1 : 1;
});
}
}
#[Route('/admin', name: 'app_panel_dashboard')]
public function index(Request $request, SellerDetailRequestDto $sellerDetailRequestDto, \App\Repository\ProductMonthlyStatsRepository $statsRepository, FinancialForecastingService $financialService, \App\Service\Analysis\StrategicForecastingService $forecastingService): Response
{
if ($request->get('year') != null) {
$date = new \DateTimeImmutable('first day of January ' . $request->get('year'));
$year = $request->get('year');
} else {
$date = new \DateTimeImmutable('first day of January this year');
$year = date('Y');
}
$mountly = $this->salesService->getEarningsByPeriot(DateZoneEnums::MONTHLY);
$weekly = $this->salesService->getEarningsByPeriot(DateZoneEnums::WEEKLY);
$yearly = $this->salesService->getEarningsByPeriot(DateZoneEnums::YEARLY);
$profitMothly = $this->getProfitByPeriots(DateZoneEnums::MONTHLY, $year);
$profitWeekly = $this->getProfitByPeriots(DateZoneEnums::WEEKLY, $year);
$months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
$monthsTr = ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'];
$array = [];
foreach ($months as $key => $month) {
$thisMonth = (int) date('m');
if ($month != $months[$thisMonth - 1]) {
$startDate = new \DateTimeImmutable('first day of ' . $months[$key] . ' ' . $year);
$endDate = new \DateTimeImmutable('last day of ' . $months[$key] . ' ' . $year);
$earn = $this->salesService->getEarningsBetwenTwoDate($startDate, $endDate);
$array[] = [
$monthsTr[$key],
$earn ? $earn : 0.00,
];
} else {
$startDate = new \DateTimeImmutable('first day of ' . $months[$thisMonth - 1] . ' ' . $year);
$endDate = new \DateTimeImmutable('now');
$array[] = [
$monthsTr[$thisMonth - 1],
$this->salesService->getEarningsBetwenTwoDate($startDate, $endDate),
];
break;
}
}
$inTransitTransferNumber = $this->stockTransferService->getNumberOfInTransitTransfers();
$allSallers = $this->entityManager->getRepository(User::class)->findAll();
$allWarehouses = $this->entityManager->getRepository(Warehouse::class)->findAll();
$allProducts = $this->entityManager->getRepository(Product::class)->findAll();
$uniqueProductCodes = [];
foreach ($allProducts as $p) {
$c = $p->getCode();
if ($c) {
$uniqueProductCodes[] = $c;
}
}
$uniqueProductCodes = array_unique($uniqueProductCodes);
sort($uniqueProductCodes);
$requestedProducts = $request->get('pproductId');
$products = [];
if (is_array($requestedProducts)) {
$products = array_values(array_filter($requestedProducts));
} elseif (!empty($requestedProducts)) {
$products = [(int) $requestedProducts];
}
if (empty($products)) {
$ps = $this->entityManager->getRepository(Product::class)
->createQueryBuilder('c')
->select('c.id')
->setMaxResults(5)
->getQuery()
->getResult();
foreach ($ps as $p) {
$products[] = (int) $p['id'];
}
}
$startDate = $request->get('pstartDate') ? new \DateTimeImmutable($request->get('pstartDate')) : new \DateTimeImmutable("30 days ago");
$finishDate = $request->get('pfinishDate') ? new \DateTimeImmutable($request->get('pfinishDate')) : new \DateTimeImmutable('now');
if (isset($this->entityManager->getRepository(Warehouse::class)->createQueryBuilder('c')->leftJoin('c.sales', 'sales')->where('sales.id is not null')->setMaxResults(1)->getQuery()->getResult()[0])) {
$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();
} else {
try {
$warehouseId = $request->get('pwarehouseId') ? $request->get('pwarehouseId') : $this->entityManager->getRepository(Warehouse::class)->createQueryBuilder('c')->leftJoin('c.sales', 'sales')->setMaxResults(1)->getQuery()->getResult()[0]->getId();
} catch (\Exception $e) {
$warehouseId = 0;
}
}
$sellerId = $request->get('psellerId');
$periyot = 0;
if ($request->get('periyot') !== null) {
if ($request->get('periyot') == 'weekly') {
$periyot = 7;
$periyotAdi = 'Hafta';
}
if ($request->get('periyot') == 'mothly') {
$periyot = 30;
$periyotAdi = 'Ay';
}
if ($request->get('periyot') == 'daily') {
$periyot = 1;
$periyotAdi = 'Gün';
}
} else {
$periyot = 7;
$periyotAdi = 'Hafta';
}
$fark = $startDate->diff($finishDate);
if ($fark->m != 0) {
$fark = $fark->m * 30 + $fark->d;
} else {
$fark = $fark->d;
}
$periyotSayisi = (int) ceil($fark / $periyot);
$arrays = [];
$baslangicTarihi = $startDate->format('Y-m-d');
$bitisTarihi = $startDate->modify('+' . $periyot . 'days')->format('Y-m-d');
for ($i = 0; $i < $periyotSayisi; $i++) {
if ($finishDate < $bitisTarihi) {
$bitisTarihi = $finishDate;
}
foreach ($products as $productId) {
$product = $this->entityManager->getRepository(Product::class)->find($productId);
if (!$product) {
continue;
}
$periotData = $this->getProductsByPeriot($baslangicTarihi, $bitisTarihi, $productId, $warehouseId, $sellerId);
$totalAmount = 0.00;
if (!empty($periotData) && isset($periotData[0]['totalAmount'])) {
$totalAmount = (float) $periotData[0]['totalAmount'];
}
$arrays[$i + 1 . '.' . $periyotAdi][$product->getName()] = $totalAmount;
}
$baslangicTarihi = $bitisTarihi;
$yeniBaglangicTarihi = new \DateTimeImmutable($baslangicTarihi);
$bitisTarihi = $yeniBaglangicTarihi->modify('+' . $periyot . 'days')->format('Y-m-d');
}
$sellers = $this->entityManager->getRepository(User::class)->findAll();
$urun = [];
$periyotlar = [];
foreach ($arrays as $key => $arrayy) {
$periyotlar[] = $key;
foreach ($arrayy as $key => $arr) {
$urun[$key][] = $arr;
}
}
$lowStockProducts = $this->getLowStockProducts();
$sellersDetail = $this->getSalesBySeller(
$sellerDetailRequestDto->getSellerStartDate(),
$sellerDetailRequestDto->getSellerFinishDate(),
$sellerDetailRequestDto->getSellerProducts(),
$sellerDetailRequestDto->getSellerId(),
$sellerDetailRequestDto->getSelectWarehouseForSellersTable(),
);
// --- NEW: 2025 Strategic Data Calculation ---
$stats2025 = $statsRepository->findBy(['year' => 2025]);
$totalRevenue2025 = 0;
$totalProfit2025 = 0;
foreach ($stats2025 as $stat) {
$totalRevenue2025 += $stat->getTotalSalesQty() * $stat->getAvgSalesPrice();
// Net Profit field might be string or float, ensure float
$cost = (float) $stat->getAvgCostPrice();
$price = (float) $stat->getAvgSalesPrice();
$qty = (float) $stat->getTotalSalesQty();
$totalProfit2025 += ($price - $cost) * $qty;
}
$selectedMonth = $request->query->getInt('analysis_month', (int) date('m'));
$statsMonth = $statsRepository->findBy(['year' => 2025, 'month' => $selectedMonth]);
$monthRevenue2025 = 0;
$monthProfit2025 = 0;
foreach ($statsMonth as $stat) {
$monthRevenue2025 += $stat->getTotalSalesQty() * $stat->getAvgSalesPrice();
$cost = (float) $stat->getAvgCostPrice();
$price = (float) $stat->getAvgSalesPrice();
$qty = (float) $stat->getTotalSalesQty();
$monthProfit2025 += ($price - $cost) * $qty;
}
// --- 2026 Projeksiyon (Aylık Gelir Özeti) Hesaplaması (2025 Bazlı) ---
$strategicRevenue2026Breakdown = [];
$strategicRevenue2026Total = 0;
// 2025 yılının tüm verilerini çekiyoruz
$allStats2025 = $statsRepository->findBy(['year' => 2025]);
// 12 aylık boş bir dizi oluştur (1-12)
$monthlyRevenues = array_fill(1, 12, 0.0);
foreach ($allStats2025 as $stat) {
$m = $stat->getMonth();
if ($m >= 1 && $m <= 12) {
$rev = (float) $stat->getTotalSalesQty() * (float) $stat->getAvgSalesPrice();
$monthlyRevenues[$m] += $rev;
}
}
foreach ($monthlyRevenues as $m => $val) {
$strategicRevenue2026Breakdown[$m] = $val;
$strategicRevenue2026Total += $val;
}
// ---------------------------------------------------------------------
// --- 2026 Projeksiyon (Aylık Kâr Özeti) Hesaplaması (2025 Bazlı) ---
$strategicProfit2026Breakdown = [];
$strategicProfit2026Total = 0;
$monthlyProfits = array_fill(1, 12, 0.0);
foreach ($allStats2025 as $stat) {
$m = $stat->getMonth();
if ($m >= 1 && $m <= 12) {
$cost = (float) $stat->getAvgCostPrice();
$price = (float) $stat->getAvgSalesPrice();
$qty = (float) $stat->getTotalSalesQty();
$monthlyProfits[$m] += ($price - $cost) * $qty;
}
}
foreach ($monthlyProfits as $m => $val) {
$strategicProfit2026Breakdown[$m] = $val;
$strategicProfit2026Total += $val;
}
// --- 2026 Gerçekleşen Veriler (Actuals) ---
$allStats2026 = $statsRepository->findBy(['year' => 2026]);
$actualRevenue2026Breakdown = array_fill(1, 12, 0.0);
$actualProfit2026Breakdown = array_fill(1, 12, 0.0);
$actualRevenue2026Total = 0;
$actualProfit2026Total = 0;
foreach ($allStats2026 as $stat) {
$m = $stat->getMonth();
if ($m >= 1 && $m <= 12) {
$rev = (float) $stat->getTotalSalesQty() * (float) $stat->getAvgSalesPrice();
$cost = (float) $stat->getAvgCostPrice();
$price = (float) $stat->getAvgSalesPrice();
$qty = (float) $stat->getTotalSalesQty();
$prof = ($price - $cost) * $qty;
$actualRevenue2026Breakdown[$m] += $rev;
$actualProfit2026Breakdown[$m] += $prof;
}
}
// Toplamları hesapla
$actualRevenue2026Total = array_sum($actualRevenue2026Breakdown);
$actualProfit2026Total = array_sum($actualProfit2026Breakdown);
// --- 2025 Ürün Bazlı Aylık Satış Raporu ---
// Veri Kaynakları:
// - Satış Miktarları: product_monthly_stats.total_sales_qty
// - Mevcut Stok: StockInfo tablosu
// - Birim Maliyet: product_monthly_stats.avg_cost_price (ağırlıklı ortalama)
$productSales2025 = [];
foreach ($allStats2025 as $stat) {
$product = $stat->getProduct();
if ($product === null) {
continue;
}
$pId = $product->getId();
if (!isset($productSales2025[$pId])) {
$productSales2025[$pId] = [
'name' => $product->getName(),
'code' => $product->getCode(),
'months' => array_fill(1, 12, 0),
'total' => 0,
// Mevcut Stok: StockInfo tablosundan
'currentStock' => array_reduce($product->getStockInfos()->toArray(), function ($sum, $item) {
return $sum + $item->getTotalQuantity();
}, 0),
// Maliyet hesaplaması için ara değerler
'totalCostSum' => 0, // Toplam maliyet (qty * cost)
'totalQtyForCost' => 0, // Toplam miktar (ağırlıklı ortalama için)
'unitPrice' => 0 // Sonradan hesaplanacak
];
}
$m = $stat->getMonth();
if ($m >= 1 && $m <= 12) {
$qty = (float) $stat->getTotalSalesQty();
$avgCostPrice = (float) $stat->getAvgCostPrice();
$productSales2025[$pId]['months'][$m] += $qty;
$productSales2025[$pId]['total'] += $qty;
// Ağırlıklı ortalama maliyet hesabı için: (qty * cost) toplamı
if ($qty > 0 && $avgCostPrice > 0) {
$productSales2025[$pId]['totalCostSum'] += ($qty * $avgCostPrice);
$productSales2025[$pId]['totalQtyForCost'] += $qty;
}
}
}
// Post-process: Ağırlıklı ortalama maliyeti hesapla ve derived values'ları oluştur
foreach ($productSales2025 as $pId => &$data) {
$currentStock = $data['currentStock'];
$yearlySales = $data['total'];
// Birim Fiyat: Ağırlıklı ortalama maliyet (product_monthly_stats'tan)
$unitPrice = 0;
if ($data['totalQtyForCost'] > 0) {
$unitPrice = $data['totalCostSum'] / $data['totalQtyForCost'];
}
$data['unitPrice'] = $unitPrice;
// --- Yearly Plan Calculations ---
// Yıllık Satılan: $yearlySales (product_monthly_stats'tan toplam)
// Mevcut Stok: $currentStock (StockInfo'dan)
// Satılan - Stok: $yearlySales - $currentStock
// Sipariş Miktarı: Satılan - Stok (negatifse 0)
// Toplam Maliyet: Sipariş Miktarı * Birim Fiyat
$salesMinusStock = $yearlySales - $currentStock;
$orderQty = max(0, ceil($salesMinusStock)); // Negatif siparişi engelle
$yearlyTotalCost = $orderQty * $unitPrice;
$data['yearly'] = [
'sales' => ceil($yearlySales), // Yıllık Satılan
'remaining' => ceil($salesMinusStock), // Satılan - Stok
'order' => $orderQty, // Sipariş Miktarı
'totalCost' => $yearlyTotalCost // Toplam
];
// --- Monthly Projection Calculations (Stok - Satılan) ---
$monthlyCalculations = [];
for ($m = 1; $m <= 12; $m++) {
$monthlySales = $data['months'][$m];
$monthlyRemaining = ceil($currentStock - $monthlySales);
$monthlyOrder = $monthlyRemaining;
$monthlyTotalCost = $monthlyOrder * $unitPrice;
$monthlyCalculations[$m] = [
'sales' => $monthlySales,
'remaining' => $monthlyRemaining,
'order' => $monthlyOrder,
'totalCost' => $monthlyTotalCost
];
}
$data['monthly_derived'] = $monthlyCalculations;
// Temizlik: Ara hesaplama alanlarını kaldır
unset($data['totalCostSum'], $data['totalQtyForCost']);
}
unset($data); // Break reference
// Çok satandan aza doğru sırala - uasort ile anahtarları koru (frontend için gerekli)
uasort($productSales2025, function ($a, $b) {
return $b['total'] <=> $a['total'];
});
// Sort allProducts based on 2025 sales totals (Added for Product Projection Table)
usort($allProducts, function ($a, $b) use ($productSales2025) {
$totalA = $productSales2025[$a->getId()]['total'] ?? 0;
$totalB = $productSales2025[$b->getId()]['total'] ?? 0;
return $totalB <=> $totalA; // DESC
});
// --- 2025-2026 Inventory Turn & Sleeping Stock Analysis (Rolling 12 Months) ---
$inventoryAnalysis = [];
// 1. Fetch Lifecycle Stats (First Seen, All-time Sales/Purchases) for ALL products
$lifecycleStats = $statsRepository->getProductLifecycleStats();
// Dinamik olarak son 12 ayı belirle
$rollingMonths = [];
$currentMonthObj = new \DateTimeImmutable('first day of this month');
for ($i = 0; $i < 12; $i++) {
$date = $currentMonthObj->modify("-$i month");
$rollingMonths[] = [
'year' => (int) $date->format('Y'),
'month' => (int) $date->format('m')
];
}
// Son 12 ayın verilerini toplu çek (Performans için)
$yearsToFetch = array_unique(array_column($rollingMonths, 'year'));
$allRecentStats = $statsRepository->findBy(['year' => $yearsToFetch]);
// Ürün bazlı grupla
$statsByProduct = [];
foreach ($allRecentStats as $s) {
if ($s->getProduct()) {
$pId = $s->getProduct()->getId();
$statsByProduct[$pId][$s->getYear()][$s->getMonth()] = $s;
}
}
foreach ($allProducts as $product) {
// Sadece Tekil Ürün ve Paket Ürün olanları dahil et (Transport, Servis vb. hariç)
$type = $product->getProductTypeEnum();
if ($type !== ProductTypeEnum::SINGLE_ITEM && $type !== ProductTypeEnum::BUNDLE) {
continue;
}
$pId = $product->getId();
// Mevcut Stok (Tüm depolardan)
$currentStock = array_reduce($product->getStockInfos()->toArray(), function ($sum, $item) {
return $sum + $item->getTotalQuantity();
}, 0);
$totalSalesLast12Months = 0;
$totalSales3Months = 0;
$totalSales6Months = 0;
$inactiveMonthsCount = 0; // Last 8 months
$revenueLast12Months = 0;
$costLast12Months = 0;
// Rolling months dizisi tersten (en eskiden en yeniye) değil, en yeniden en eskiye doğru
foreach ($rollingMonths as $index => $period) {
$s = $statsByProduct[$pId][$period['year']][$period['month']] ?? null;
$qty = $s ? (float) $s->getTotalSalesQty() : 0;
$totalSalesLast12Months += $qty;
// Ek kolonlar için (3 Ay, 6 Ay)
if ($index < 3) {
$totalSales3Months += $qty;
}
if ($index < 6) {
$totalSales6Months += $qty;
}
// Hareketsizlik kontrolü (Son 8 ay için)
if ($index < 8) {
if ($qty <= 0) {
$inactiveMonthsCount++;
}
}
if ($s) {
$revenueLast12Months += $qty * (float) $s->getAvgSalesPrice();
$costLast12Months += $qty * (float) $s->getAvgCostPrice();
}
}
// Eğer verilerde maliyet/fiyat yoksa ürün kartından al
$avgCostLast12 = $totalSalesLast12Months > 0 ? $costLast12Months / $totalSalesLast12Months : (float) ($product->getPurchasePrice() ?? 0);
$avgPriceLast12 = $totalSalesLast12Months > 0 ? $revenueLast12Months / $totalSalesLast12Months : (float) ($product->getSalePrice() ?? 0);
// 1. Satış Hızı (Velocity) - Aylık Ortalama
$velocity = $totalSalesLast12Months / 12;
// Lifecycle Verileri
$lStats = $lifecycleStats[$pId] ?? null;
$isNewProduct = false;
$allTimeSellThrough = 0;
if ($lStats) {
// Yeni Ürün Kontrolü: < 3 Ay (First Seen Date)
$firstSeenDate = new \DateTime($lStats['firstSeen'] . '-01'); // YYYY-MM-01
$threeMonthsAgo = (new \DateTime())->modify('-3 months');
if ($firstSeenDate > $threeMonthsAgo) {
$isNewProduct = true;
}
// Sell-Through Rate (All Time)
if ($lStats['totalPurchases'] > 0) {
$allTimeSellThrough = $lStats['totalSales'] / $lStats['totalPurchases'];
}
} else {
// İstatistik tablosunda hiç kaydı yoksa (veya eski değilse) Yeni Ürün kabul edebiliriz
$isNewProduct = true;
}
// 2. Sleeping Stock & Status Logic (Advanced)
$sleepingStock = 0;
$status = 'Normal';
if ($isNewProduct) {
$status = 'Yeni Ürün';
} else {
// Atıl Stok Kontrolü: 8 aydır satış yok
if ($inactiveMonthsCount >= 8 && $currentStock > 0) {
$status = 'Atıl Stok (8 Ay Hareketsiz)';
$sleepingStock = $currentStock; // Hepsi uyuyan kabul edilir
} else {
// Uyuyan Stok (Coverage & Turnover Logic)
// Stok bizi kaç ay idare eder? (Stock Coverage)
$stockCoverageMonths = $velocity > 0 ? ($currentStock / $velocity) : 999;
// Kural 1: 12 Aydan fazla yetecek stok varsa -> Fazlası uyuyandır
if ($stockCoverageMonths > 12) {
$neededStock = $velocity * 12; // 1 senelik stok makul
$sleepingStock = max(0, $currentStock - $neededStock);
if ($sleepingStock > 0) {
$status = 'Uyuyan Stok';
}
}
// Kural 2: 6 Aydan fazla yetecek stok var VE Satış Performansı (Sell-Through) %20'nin altındaysa
elseif ($stockCoverageMonths > 6 && $allTimeSellThrough < 0.20) {
$neededStock = $velocity * 6; // 6 aylık stok makul
$sleepingStock = max(0, $currentStock - $neededStock);
if ($sleepingStock > 0) {
$status = 'Uyuyan Stok (Düşük Performans)';
}
} elseif ($currentStock <= ($velocity * 1.5) && $velocity > 0) {
$status = 'Hızlı Dönüş (Kritik)';
}
}
}
// 4. Yatırım Verimliliği (ROI)
// Bağlı Sermaye: Mevcut Stok * Maliyet
$tiedCapital = $currentStock * $avgCostLast12;
// Verimlilik: Son 12 aylık ciro / Bağlı sermaye (veya benzeri bir oran)
$efficiency = $tiedCapital > 0 ? ($revenueLast12Months / $tiedCapital) : ($totalSalesLast12Months > 0 ? 99 : 0);
if ($currentStock > 0 || $totalSalesLast12Months > 0) {
$inventoryAnalysis[] = [
'name' => $product->getName(),
'code' => $product->getCode(),
'endStock' => $currentStock,
'totalSales' => $totalSalesLast12Months,
'totalSales3Months' => $totalSales3Months,
'totalSales6Months' => $totalSales6Months,
'velocity' => $velocity,
'sleepingStock' => $sleepingStock,
'status' => $status,
'avgCost' => $avgCostLast12,
'avgPrice' => $avgPriceLast12,
'sleepingStockCost' => $sleepingStock * $avgCostLast12,
'sleepingStockRevenue' => $sleepingStock * $avgPriceLast12,
'efficiency' => $efficiency,
'inactiveMonths' => $inactiveMonthsCount
];
}
}
// Uyuyan stok miktarına göre azalan sırala
usort($inventoryAnalysis, function ($a, $b) {
return $b['sleepingStock'] <=> $a['sleepingStock'];
});
// --- Budget Planning Data (Aylık Bütçe Detayı) ---
$budgetPlanningData = $financialService->generateFinancialForecast(2025, 2026, true);
// --------------------------------------------
return $this->render('admin/dashboard/index.html.twig', [
'controller_name' => 'DashboardController',
'mountly' => CurrencyHelper::convertToCurrency($mountly),
'weekly' => CurrencyHelper::convertToCurrency($weekly),
'profitMonthly' => $profitMothly,
'profitWeekly' => $profitWeekly,
'yearly' => $yearly,
'inTransits' => $inTransitTransferNumber,
'earningInPeriots' => $array,
'sallers' => $this->userService->getTotalUsersEarns(),
'allSallers' => $allSallers,
'allWarehouses' => $allWarehouses,
'allProducts' => $allProducts,
'periyotlar' => $periyotlar,
'urunler' => $urun,
'monthsTr' => $monthsTr,
'sellers' => $sellers,
'sellersDetail' => $sellersDetail,
'warehouses' => $this->warehouseService->getAll(),
'lowStockProducts' => $lowStockProducts,
// New Variables (Strategic)
'strategic_2025_revenue' => $totalRevenue2025,
'strategic_2025_profit' => $totalProfit2025,
'strategic_month_revenue' => $monthRevenue2025,
'strategic_month_profit' => $monthProfit2025,
'selected_analysis_month' => $selectedMonth,
'strategic_revenue_2026_breakdown' => $strategicRevenue2026Breakdown,
'strategic_revenue_2026_total' => $strategicRevenue2026Total,
// New Variables (Actuals & Profit Projection)
'strategic_profit_2026_breakdown' => $strategicProfit2026Breakdown,
'strategic_profit_2026_total' => $strategicProfit2026Total,
'actual_revenue_2026_breakdown' => $actualRevenue2026Breakdown,
'actual_revenue_2026_total' => $actualRevenue2026Total,
'actual_profit_2026_breakdown' => $actualProfit2026Breakdown,
'actual_profit_2026_total' => $actualProfit2026Total,
'product_sales_2025' => $productSales2025,
'inventory_analysis' => $inventoryAnalysis,
// Budget Planning Data (Aylık Bütçe Detayı)
'budget_planning' => $budgetPlanningData,
'allMeasurementUnits' => $this->entityManager->getRepository(\App\Entity\MeasurementUnits::class)->findAll(),
'allMeasurements' => $this->entityManager->getRepository(\App\Entity\Measurements::class)->findAll(),
'uniqueProductCodes' => $uniqueProductCodes,
]);
}
public function getLowStockProducts()
{
$qb = $this->entityManager->getRepository(StockInfo::class)->createQueryBuilder('si');
$qb
->leftJoin('si.product', 'p')
->leftJoin('si.warehouse', 'w')
->leftJoin('p.measurementUnit', 'mu')
->leftJoin('p.measurement', 'm')
->where(
$qb->expr()->orX(
// Eğer symbol 'SET' ise totalQuantity 200 altı olanları getir
$qb->expr()->andX(
$qb->expr()->eq('mu.symbol', ':setSymbol'),
$qb->expr()->lt('si.totalQuantity', ':setThreshold')
),
// Aksi durumda totalQuantity 300 altı olanları getir
$qb->expr()->andX(
$qb->expr()->neq('mu.symbol', ':setSymbol'),
$qb->expr()->lt('si.totalQuantity', ':defaultThreshold')
)
)
)
->setParameter('setSymbol', 'SET')
->setParameter('setThreshold', 200)
->setParameter('defaultThreshold', 300)
->select(
'p.id as productId',
'p.name as name',
'p.code as productCode',
'm.name as measurement',
'si.totalQuantity as totalQuantity',
'si.id as siId',
'w.name as warehouseName',
'mu.name as measurementUnit',
'w.id as warehouseId',
)
->orderBy('si.totalQuantity', 'desc');
$result = $qb->getQuery()->getResult();
$dates = $this->getDatesFromOneYear();
for ($i = 0; $i < count($result); $i++) {
foreach ($dates as $date) {
$product = $this->productServiceImpl->getEntityId($result[$i]['productId']);
$price = $this->salesProductsServiceImpl->getQuantityOfProductSoldBy($date['start'], $date['end'], $product, $result[$i]['warehouseId']);
$result[$i]['quantities'][] = $price;
$result[$i]['uuid'] = str_replace(".", "", uniqid('', true));
}
}
// dd($result);
// dd($this->getDatesFromOneYear());
return $result;
}
#[Route('/admin/dashboard/product/sold/details/{productId}/{warehouseId}', name: 'admin_dashboard_product_sold_details')]
public function getQuantityOfSoldProductDetails(Request $request, $productId, $warehouseId)
{
$dates = $this->getDatesFromOneYear();
$product = $this->productServiceImpl->getEntityId($productId);
$warehouse = null;
if ($warehouseId != null && $warehouseId != 0) {
$warehouse = $this->warehouseService->getEntityById($warehouseId);
}
$result = [];
for ($i = 0; $i < count($dates); $i++) {
$month = DateUtil::getMonthOfTurkish((int) (new \DateTimeImmutable($dates[$i]['start']))->format('m'));
$quantity = $this->salesProductsServiceImpl->getQuantityOfProductSoldBy(
$dates[$i]['start'],
$dates[$i]['end'],
$product,
$warehouse
);
if ($quantity == null)
$quantity = 0.00;
else
$quantity = CurrencyHelper::roundUp($quantity);
$result[$month] = [
'quantity' => $quantity,
'measurement' => $product->getMeasurementUnit()->getSymbol(),
'startDate' => $dates[$i]['start'],
'endDate' => $dates[$i]['end']
];
}
return new JsonResponse($result);
}
public function getDatesFromOneYear()
{
$dates = [];
$today = new \DateTimeImmutable();
// Son bir yıl boyunca her ay için başlangıç ve bitiş tarihi
for ($i = 0; $i < 12; $i++) {
// Geçmişe doğru git
$monthStart = $today->modify("-$i month")->modify('first day of this month');
$monthEnd = $monthStart->modify('last day of this month');
$dates[] = [
'month' => 12 - $i,
'start' => $monthStart->format('Y-m-d'), // Başlangıç tarihi
'end' => $monthEnd->format('Y-m-d') // Bitiş tarihi
];
}
return $dates;
}
public function getSalesBySeller($startDate = null, $finishDate = null, $products = null, $sellerIds = null, $warehouseId = null)
{
$year = date('Y');
if ($startDate == null)
$startDate = new \DateTimeImmutable('first day of january ' . $year);
else {
$startDate = \DateTimeImmutable::createFromFormat('Y-m-d', $startDate);
}
if ($finishDate == null)
$finishDate = new \DateTimeImmutable('last day of december ' . $year);
else {
$finishDate = \DateTimeImmutable::createFromFormat('Y-m-d', $finishDate);
}
$userRepo = $this->entityManager->getRepository(User::class);
$canSeeHidden = $this->canCurrentUserSeeHiddenSales();
$qb = $userRepo->createQueryBuilder('u')
->leftJoin('u.sales', 's')
->leftJoin('s.warehouse', 'w')
->leftJoin('s.productsSolds', 'ps')
->leftJoin('ps.product', 'p')
->where('s.salesDate >= :startDate')
->andWhere('s.salesDate <= :finishDate')
->setParameter('startDate', $startDate)
->setParameter('finishDate', $finishDate);
if (!$canSeeHidden) {
$qb->andWhere('s.visible = :visible')
->setParameter('visible', true);
}
if ($sellerIds != null) {
$qb->andWhere(
$qb->expr()->in('u.id', $sellerIds)
);
}
if ($products != null) {
$qb->andWhere(
$qb->expr()->in('product.id', $products)
);
}
if ($warehouseId != null && $warehouseId != 0) {
$qb->andWhere(
$qb->expr()->eq('w.id', $warehouseId)
);
}
$result = $qb->getQuery()->getResult();
$array = [];
/**
* @var User $user
*/
foreach ($result as $user) {
$totalPurchasePrice = 0.00;
$totalProfit = 0.00;
foreach ($user->getSales() as $sale) {
if (!$canSeeHidden && !$sale->isVisible()) {
continue;
}
if ($sale->getSalesDate() >= $startDate && $sale->getSalesDate() <= $finishDate) {
foreach ($sale->getProductsSolds() as $productsSold) {
$totalProfit += $this->salesProductsServiceImpl->calculateProfitByProductSold($productsSold);
$totalPurchasePrice += $productsSold->getTotalPuchasePrice();
}
}
}
$array[] = [
'userId' => $user->getId(),
'startDate' => $startDate->format('d-m-Y'),
'finishDate' => $finishDate->format('d-m-Y'),
'username' => $user->getUsername(),
'email' => $user->getEmail(),
'totalPurchasePrice' => $totalPurchasePrice,
'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
$startDate,
$finishDate,
$warehouseId,
$user->getId()
)
];
}
return $array;
}
#[Route('/get-product-by-productname', name: 'get_product_by_product_name')]
public function getProductByProductName(Request $request)
{
$search = $request->get('search');
$qb = $this->entityManager->getRepository(Product::class)
->createQueryBuilder('p')
->where('p.name LIKE :s')
->setParameter('s', '%' . $search . '%')
->getQuery()
->getResult();
return $this->json($qb);
}
#[Route('/dashboard/weeaks-earning/{warehouseId}', name: 'dashboard_week_earning')]
public function getWeekEarningByWeek(Request $request, EntityManagerInterface $entityManager, $warehouseId)
{
$firstDayOfWeek = trim(explode('*', $request->get('week'))[0]);
$lastDayOfWeek = trim(explode('*', $request->get('week'))[1]);
$firstDayOfWeek = \DateTimeImmutable::createFromFormat('Y-m-d', $firstDayOfWeek);
$lastDayOfWeek = \DateTimeImmutable::createFromFormat('Y-m-d', $lastDayOfWeek);
$productsSoldRepo = $entityManager->getRepository(ProductsSold::class);
$canSeeHidden = $this->canCurrentUserSeeHiddenSales();
$qb = $productsSoldRepo->createQueryBuilder('c')
->leftJoin('c.sales', 'sales')
->leftJoin('c.product', 'product')
->where('sales.salesDate >= :firstDayOfWeek')
->andWhere('sales.salesDate <= :lastDayOfWeek')
->setParameters(
[
'firstDayOfWeek' => $firstDayOfWeek,
'lastDayOfWeek' => $lastDayOfWeek
]
);
if (!$canSeeHidden) {
$qb->andWhere('sales.visible = :visible')
->setParameter('visible', true);
}
$qb = $qb->getQuery()->getResult();
$period = new DatePeriod(
date_create_from_format('Y-m-d', $firstDayOfWeek->format('Y-m-d')),
new DateInterval('P1D'),
date_create_from_format('Y-m-d', $lastDayOfWeek->format('Y-m-d'))->add(new DateInterval('P1D'))
);
$days = [];
foreach ($period as $key => $value) {
$days[] = $value->format('Y-m-d');
}
if ($warehouseId == 0)
$warehouseId = null;
return new JsonResponse(
[
'totalEarning' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate(
$firstDayOfWeek,
$lastDayOfWeek,
$warehouseId,
),
'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
$firstDayOfWeek,
$lastDayOfWeek,
$warehouseId,
),
'days' => $days,
]
);
}
#[Route('/dashboard/day-earning/{warehouseId}', name: 'dashboard_day_earning', methods: ['GET', 'POST'])]
public function getEarningByDay(Request $request, EntityManagerInterface $entityManager, $warehouseId)
{
$day = $request->get('day');
$day = \DateTimeImmutable::createFromFormat('Y-m-d', $day);
$productsSoldRepo = $entityManager->getRepository(ProductsSold::class);
$canSeeHidden = $this->canCurrentUserSeeHiddenSales();
$qb = $productsSoldRepo->createQueryBuilder('c')
->leftJoin('c.sales', 'sales')
->leftJoin('c.product', 'product')
->where('sales.salesDate = :day')
->setParameter('day', $day->format('Y-m-d'));
if (!$canSeeHidden) {
$qb->andWhere('sales.visible = :visible')
->setParameter('visible', true);
}
$qb = $qb->getQuery()->getResult();
if ($warehouseId == 0)
$warehouseId = null;
return new JsonResponse(
[
'totalEarning' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate(
$day,
$day,
$warehouseId
),
'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
$day,
$day,
$warehouseId
),
]
);
}
#[Route('/dashboard/year-earning/{warehouseId}', name: 'dashboard_year_earning')]
public function getEarningByYear(Request $request, EntityManagerInterface $entityManager, $warehouseId)
{
$year = $request->get('year');
$firstDayOfYear = new \DateTimeImmutable('first day of january ' . $year);
$lastDayOfYear = new \DateTimeImmutable('last day of december ' . $year . ' 23:59:59');
// $productsSoldRepo = $entityManager->getRepository(ProductsSold::class);
// $qb = $productsSoldRepo->createQueryBuilder('c')
// ->leftJoin('c.sales', 'sales')
// ->leftJoin('c.product', 'product')
// ->where('sales.salesDate >= :firstDayOfYear')
// ->setParameter('firstDayOfYear',$firstDayOfYear)
// ->andWhere('sales.salesDate <= :lastDayOfYear')
// ->setParameter('lastDayOfYear', $lastDayOfYear);
//
// if($request->get('all-warehouses') == null){
//
// $qb->andWhere('sales.warehouse = :warehouse')
// ->setParameter('warehouse',$this->warehouseService->getMainWarehouse());
// }
//
// $qb = $qb->getQuery()->getResult();
//
// $totalEarningsArr = array_map(function ($val){
// return $val->getTotalPuchasePrice();
// }, $qb);
//
// $totalEarning = 0.00;
// foreach ($totalEarningsArr as $totalEarn){
// $totalEarning += $totalEarn;
// }
//
// $profits = array_map(function ($val){
// return $val->getTotalPuchasePrice() - $val->getTotalPrice();
// },$qb);
//
//
// $totalProfit = 0.00;
//
// foreach ($profits as $profit){
// $totalProfit += $profit;
// }
return new JsonResponse(
[
'totalEarning' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDateAndByWarehouse(
$firstDayOfYear,
$lastDayOfYear,
$warehouseId == 0 ? null : $warehouseId,
),
'totalProfit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate(
$firstDayOfYear,
$lastDayOfYear,
$warehouseId == 0 ? null : $warehouseId,
),
]
);
}
#[Route('/dashboard/month-earning/{warehouseId}', name: 'dashboard_month_earning')]
public function getEarningByMonth(Request $request, EntityManagerInterface $entityManager, $warehouseId)
{
if ($request->get('year') != null) {
$year = $request->get('year');
} else {
$year = date('Y');
}
$months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
$month = $request->get('month');
$firstDayOfMounth = (new \DateTimeImmutable('first day of ' . $months[$month - 1] . ' ' . $year))->format('Y-m-d');
$lastDayOfMount = (new \DateTimeImmutable('last day of ' . $months[$month - 1] . ' ' . $year))->format('Y-m-d');
$firstWeekStartDay = $firstDayOfMounth;
$firstWeekFinishDay = (new \DateTime('+6 days ' . $firstDayOfMounth . ' ' . $year))->format('Y-m-d');
$secondWeekStartDay = (new \DateTime('+1 days ' . $firstWeekFinishDay . ' ' . $year))->format('Y-m-d');
$secondWeekFinishDay = (new \DateTime('+6 days ' . $secondWeekStartDay . ' ' . $year))->format('Y-m-d');
$thirdWeekStartDay = (new \DateTime('+1 days ' . $secondWeekFinishDay . ' ' . $year))->format('Y-m-d');
$thirdWeekFinishDay = (new \DateTime('+6 days ' . $thirdWeekStartDay . ' ' . $year))->format('Y-m-d');
$fourthWeekStartDate = (new \DateTime('+1 days ' . $thirdWeekFinishDay . ' ' . $year))->format('Y-m-d');
$fourthWeekFinishDate = $lastDayOfMount;
$firstWeek = $firstWeekStartDay . " * " . $firstWeekFinishDay;
$secondWeek = $secondWeekStartDay . " * " . $secondWeekFinishDay;
$thirdWeek = $thirdWeekStartDay . " * " . $thirdWeekFinishDay;
$fourthWeek = $fourthWeekStartDate . " * " . $fourthWeekFinishDate;
$productsSoldRepo = $entityManager->getRepository(ProductsSold::class);
$canSeeHidden = $this->canCurrentUserSeeHiddenSales();
$qb = $productsSoldRepo->createQueryBuilder('c')
->leftJoin('c.sales', 'sales')
->leftJoin('c.product', 'product')
->where('sales.salesDate >= :firstDayOfMonth')
->andWhere('sales.salesDate <= :lastDayOfMonth')
->setParameters(
[
'firstDayOfMonth' => $firstDayOfMounth,
'lastDayOfMonth' => $lastDayOfMount
]
)
->orderBy('sales.salesDate', 'DESC');
if (!$canSeeHidden) {
$qb->andWhere('sales.visible = :visible')
->setParameter('visible', true);
}
$qb = $qb->getQuery()->getResult();
$data = [];
foreach ($qb as $val) {
$data[] = [
$val->getProduct()->getCode(),
$val->getProductName(),
$val->getQuantity(),
$val->getUnitPriceFob(),
$val->getUnitPriceNavlun(),
$val->getTotalUnitPrice(),
$val->getTotalPrice(),
$val->getTotalPuchasePrice()
];
}
$startDate = (new \DateTimeImmutable('first day of ' . $months[$month - 1] . ' ' . $year));
$finishDate = (new \DateTimeImmutable('last day of ' . $months[$month - 1] . ' ' . $year));
if ($warehouseId == 0)
$warehouseId = null;
return new JsonResponse(
[
'profit' => $this->salesServiceImpl->getTotalSalesProfitBetweenTwoDate($startDate, $finishDate, $warehouseId),
'totalPrices' => $this->salesServiceImpl->getTotalSalesPriceBetweenTwoDate($startDate, $finishDate, $warehouseId),
'data' => $data,
'firstWeek' => $firstWeek,
'secondWeek' => $secondWeek,
'thirdWeek' => $thirdWeek,
'fourthWeek' => $fourthWeek
]
);
}
private function getProductsByPeriot($startdate, $finishDate, $product, $warehouse, $seller)
{
$repo = $this->entityManager->getRepository(Sales::class);
$canSeeHidden = $this->canCurrentUserSeeHiddenSales();
$result = $repo->createQueryBuilder('c')
->leftJoin('c.productsSolds', 'productsSolds')
->leftJoin('productsSolds.product', 'product')
->leftJoin('c.warehouse', 'warehouse');
if ($startdate != null) {
$result->where('c.salesDate >= :startDate')
->setParameter('startDate', $startdate);
}
if ($finishDate != null) {
$result->andWhere('c.salesDate <= :finishDate')
->setParameter('finishDate', $finishDate);
}
if ($product != 0) {
$result->andWhere('product.id = :product')
->setParameter('product', $product);
}
if ($warehouse != null) {
$result->andWhere('warehouse.id = :warehouseId')
->setParameter('warehouseId', $warehouse);
}
if ($seller != 0) {
$result->andWhere('c.seller = :seller')
->setParameter('seller', $seller);
}
if (!$canSeeHidden) {
$result->andWhere('c.visible = :visible')
->setParameter('visible', true);
}
$result
->select(['product.name as productName', 'sum(c.totalPurchasePrice) totalAmount']);
return $result->getQuery()->getResult();
}
private function canCurrentUserSeeHiddenSales(): bool
{
$user = $this->getUser();
if (!$user) {
return false;
}
$identifier = null;
if (method_exists($user, 'getUserIdentifier')) {
$identifier = $user->getUserIdentifier();
} elseif (method_exists($user, 'getUsername')) {
$identifier = $user->getUsername();
}
if ($identifier === null) {
return false;
}
return in_array(strtolower((string) $identifier), ['gizli@kullanici.com', 'azad@azad.com']);
}
#[Route('/{locale}', name: 'app_change_locale', priority: -10)]
public function changeLocale(Request $request, $locale, RouterInterface $router)
{
$params = $this->getRefererParams($request, $router);
$locales = $this->getParameter('kernel.enabled_locales');
if (in_array($locale, $locales)) {
if ($this->getUser())
$this->userService->changeLocale($this->getUser(), $locale);
$request->getSession()->set('_locale', $locale);
return $this->redirect($params);
}
$this->createNotFoundException(printf('%s locale is not avaible', $locale));
return $this->redirectToRoute('app_panel_dashboard');
}
#[Route('/dashboard/product-sales-2025-ajax', name: 'get_product_sales_2025_ajax')]
public function getProductSales2025Ajax(Request $request, ProductRepository $productRepository, ProductMonthlyStatsRepository $statsRepo, \App\Service\Analysis\SalesSeasonalityService $seasonalityService, \App\Repository\SettingsRepository $settingsRepository, \App\Service\Analysis\StrategicForecastingService $forecastingService): JsonResponse
{
// Client-side DataTable - tüm veriyi döndürüyoruz (pagination yok)
$searchParams = $_GET['search'] ?? [];
$searchValue = $searchParams['value'] ?? null;
$qb = $productRepository->createQueryBuilder('p')
->leftJoin(\App\Entity\ProductMonthlyStats::class, 's', \Doctrine\ORM\Query\Expr\Join::WITH, 's.product = p.id AND s.year = 2025')
->leftJoin('p.measurementUnit', 'mu')
->groupBy('p.id')
->select('p.id', 'p.name', 'p.code', 'mu.symbol as unitSymbol')
->addSelect('SUM(CASE WHEN s.month = 1 THEN s.totalSalesQty ELSE 0 END) as m1')
->addSelect('SUM(CASE WHEN s.month = 2 THEN s.totalSalesQty ELSE 0 END) as m2')
->addSelect('SUM(CASE WHEN s.month = 3 THEN s.totalSalesQty ELSE 0 END) as m3')
->addSelect('SUM(CASE WHEN s.month = 4 THEN s.totalSalesQty ELSE 0 END) as m4')
->addSelect('SUM(CASE WHEN s.month = 5 THEN s.totalSalesQty ELSE 0 END) as m5')
->addSelect('SUM(CASE WHEN s.month = 6 THEN s.totalSalesQty ELSE 0 END) as m6')
->addSelect('SUM(CASE WHEN s.month = 7 THEN s.totalSalesQty ELSE 0 END) as m7')
->addSelect('SUM(CASE WHEN s.month = 8 THEN s.totalSalesQty ELSE 0 END) as m8')
->addSelect('SUM(CASE WHEN s.month = 9 THEN s.totalSalesQty ELSE 0 END) as m9')
->addSelect('SUM(CASE WHEN s.month = 10 THEN s.totalSalesQty ELSE 0 END) as m10')
->addSelect('SUM(CASE WHEN s.month = 11 THEN s.totalSalesQty ELSE 0 END) as m11')
->addSelect('SUM(CASE WHEN s.month = 12 THEN s.totalSalesQty ELSE 0 END) as m12')
->addSelect('SUM(s.totalSalesQty) as total');
// Arama Filtresi (opsiyonel - client-side yapıyor ama server-side de filtre olabilir)
if ($searchValue) {
$qb->where('p.name LIKE :search OR p.code LIKE :search')
->setParameter('search', '%' . $searchValue . '%');
}
// Varsayılan sıralama
$qb->orderBy('total', 'DESC');
// TÜM VERİYİ ÇEK (Client-side için pagination yok)
$dbResults = $qb->getQuery()->getResult();
// --- STOK VERİSİNİ OPTİMİZE EDEREK ÇEKME (Bulk Fetch) ---
$productIds = [];
foreach ($dbResults as $r) {
$productIds[] = $r['id'];
}
$stockMap = [];
if (!empty($productIds)) {
$stockResults = $this->entityManager->getRepository(\App\Entity\StockInfo::class)->createQueryBuilder('si')
->select('IDENTITY(si.product) as pid, SUM(si.totalQuantity) as totalStock')
->where('si.product IN (:pids)')
->setParameter('pids', $productIds)
->groupBy('si.product')
->getQuery()
->getResult();
foreach ($stockResults as $sr) {
$stockMap[$sr['pid']] = (float) $sr['totalStock'];
}
}
// --------------------------------------------------------
// Para Birimi Ayarlarını Çek
$settings = $settingsRepository->findOneBy([]);
$currencyCode = $settings ? $settings->getCurrency() : 'EUR';
$currencySymbol = match ($currencyCode) {
'EUR' => '€',
'USD' => '$',
'TRY', 'TL' => '₺',
'GBP' => '£',
default => $currencyCode
};
// Verileri İşle ve Ham Haliyle Hazırla
$processedRows = [];
$stockRepo = $this->entityManager->getRepository(\App\Entity\Stock::class);
foreach ($dbResults as $row) {
$currentStock = $stockMap[$row['id']] ?? 0;
// --- UNIT PRICE HESAPLAMA (Son 6 Girişin Ortalaması) ---
// N+1 olacak ama şimdilik en güvenilir yöntem.
// İleride performans sorunu olursa Native SQL ile optimize edilebilir.
$lastStocks = $stockRepo->findBy(
['product' => $row['id']],
['buyingDate' => 'DESC'],
6
);
$averageUnitPrice = 0;
if (count($lastStocks) > 0) {
$totalPriceSum = 0;
foreach ($lastStocks as $stockItem) {
$totalPriceSum += $stockItem->getTotalUnitPrice();
}
$averageUnitPrice = $totalPriceSum / count($lastStocks);
}
$row['unitPrice'] = $averageUnitPrice;
// -------------------------------------------------------
// Mevsimsellik Analizi Verileri
$monthlySalesVars = [];
for ($k = 1; $k <= 12; $k++) {
$monthlySalesVars[$k] = (float) $row['m' . $k];
}
// Stok Ömrü Hesaplama
$stockDurationDays = 0;
if ($currentStock <= 0) {
$stockDurationDays = -1; // Tükendi
} else {
$totalAnnualSales = array_sum($monthlySalesVars);
if ($totalAnnualSales <= 0) {
$stockDurationDays = 99999; // Sonsuz
} else {
$tempStock = $currentStock;
$daysAccumulated = 0;
$calcMonthIndex = (int) date('n');
$calcCurrentDay = (int) date('j');
$safetyBreak = 730;
while ($tempStock > 0 && $daysAccumulated < $safetyBreak) {
$monthlySales = $monthlySalesVars[$calcMonthIndex];
$daysInMonth = 30;
if ($daysAccumulated == 0) {
$daysRemaining = max(0, $daysInMonth - $calcCurrentDay);
} else {
$daysRemaining = $daysInMonth;
}
$dailyRate = $monthlySales / $daysInMonth;
$needed = $dailyRate * $daysRemaining;
if ($needed <= 0) {
$daysAccumulated += $daysRemaining;
} else {
if ($tempStock >= $needed) {
$tempStock -= $needed;
$daysAccumulated += $daysRemaining;
} else {
$daysLasted = ($dailyRate > 0) ? ($tempStock / $dailyRate) : 0;
$daysAccumulated += $daysLasted;
$tempStock = 0;
break;
}
}
$calcMonthIndex++;
if ($calcMonthIndex > 12)
$calcMonthIndex = 1;
}
$stockDurationDays = $daysAccumulated;
}
}
$processedRows[] = [
'dbRow' => $row,
'currentStock' => $currentStock,
'stockDurationDays' => $stockDurationDays,
'indices' => $seasonalityService->calculateSeasonalityIndices($monthlySalesVars),
'monthlySalesVars' => $monthlySalesVars
];
}
// Client-side DataTable - PHP tarafında sıralama veya pagination yok
// Tüm veri işlenip döndürülüyor
$data = [];
$currentMonth = (int) date('n');
foreach ($processedRows as $pRow) {
$row = $pRow['dbRow'];
$currentStock = $pRow['currentStock'];
$stockDurationDays = $pRow['stockDurationDays'];
$indices = $pRow['indices'];
$monthlySalesVars = $pRow['monthlySalesVars'];
$unitSymbol = $row['unitSymbol'] ?? '';
// Client-side sorting için orthogonal data formatı: { display: HTML, sort: numeric }
$item = [
'name' => [
'display' => '<strong>' . htmlspecialchars($row['name']) . '</strong><br><small class="text-muted">' . htmlspecialchars($row['code']) . '</small>',
'sort' => $row['name']
],
];
$item['currentStock'] = [
'display' => '<span class="badge badge-info" style="font-size: 0.9rem;">' . number_format($currentStock, 0, ',', '.') . ' ' . $unitSymbol . '</span>',
'sort' => (int) $currentStock
];
// Stok Süresi Görselleştirme
$stockDurationHtml = '';
$stockStatus = 'success'; // default: yeşil (iyi durum)
if ($stockDurationDays == -1) {
$stockDurationHtml = '<span class="badge badge-secondary" style="background-color: #333;">Tükendi</span>';
$stockStatus = 'empty';
} elseif ($stockDurationDays == 99999) {
$stockDurationHtml = '<span class="badge badge-secondary">∞ (Satış Yok)</span>';
$stockStatus = 'noSales';
} else {
$badgeClass = 'badge-success'; // > 90 gün
if ($stockDurationDays < 45) { // < 45 gün KIRMIZI
$badgeClass = 'badge-danger';
$stockStatus = 'danger';
} elseif ($stockDurationDays < 90) { // 45-90 gün SARI
$badgeClass = 'badge-warning';
$stockStatus = 'warning';
}
$displayDays = $stockDurationDays >= 730 ? '> 2 Yıl' : number_format($stockDurationDays, 0) . ' Gün';
$stockDurationHtml = '<span class="badge ' . $badgeClass . '">' . $displayDays . '</span>';
}
// Sıralama için mantıklı değerler:
// Tükendi (-1) → 0 (en kritik, en üstte olmalı)
// Normal değerler → gerçek gün sayısı
// Satış Yok (99999) → 999999 (sonsuz, en altta olmalı)
$sortValue = $stockDurationDays;
if ($stockDurationDays == -1) {
$sortValue = 0; // Tükendi = en kritik
} elseif ($stockDurationDays == 99999) {
$sortValue = 999999; // Satış yok = pratik olarak sonsuz
}
$item['stockDuration'] = [
'display' => $stockDurationHtml,
'sort' => $sortValue,
'status' => $stockStatus // Filtreleme için: empty, noSales, danger, warning, success
];
// Filtreleme için ek metadata
$item['_meta'] = [
'stockStatus' => $stockStatus,
'stockValue' => (int) $currentStock,
'productName' => $row['name'],
'productCode' => $row['code']
];
// Ayları ekle
for ($i = 1; $i <= 12; $i++) {
$val = $monthlySalesVars[$i];
$idx = $indices[$i];
$style = '';
if ($i < $currentMonth) {
$style = 'background-color: #ffebee; color: #c62828;';
} elseif ($i === $currentMonth) {
$style = 'background-color: #e8f5e9; color: #2e7d32;';
}
if ($val > 0) {
$formattedVal = number_format($val, 0, ',', '.');
$formattedIdx = number_format($idx, 2);
$content = $formattedVal . ' <small style="font-size: 0.7em; opacity: 0.8;">/ ' . $formattedIdx . 'x</small>';
} else {
$content = '<span style="opacity: 0.5;">-</span>';
}
if ($style !== '') {
$displayContent = '<div style="' . $style . ' border-radius: 5px; padding: 6px 0;">' . $content . '</div>';
} else {
$displayContent = $content;
}
$item['m' . $i] = [
'display' => $displayContent,
'sort' => (float) $val
];
}
// Toplam
$total = (float) $row['total'];
$item['total'] = [
'display' => '<span class="font-weight-bold text-primary">' . number_format($total, 0, ',', '.') . '</span>',
'sort' => $total
];
// --- TAVSİYE EDİLEN STOK ALIMI ve MALİYETİ ---
// --- TAVSİYE EDİLEN STOK ALIMI ve MALİYETİ (Merkezi Mantık) ---
$unitPrice = (float) $row['unitPrice'];
// Mevcut ay ve sonraki 2 ayın taleplerini al (Döngüsel)
$getDemand = function ($m) use ($monthlySalesVars) {
$idx = (($m - 1) % 12) + 1;
return $monthlySalesVars[$idx] ?? 0;
};
$d1 = $getDemand($currentMonth);
$d2 = $getDemand($currentMonth + 1);
$d3 = $getDemand($currentMonth + 2);
// Merkezi Servis ile Analiz
$stockAnalysis = $forecastingService->checkStockStatus((float) $currentStock, $d1, $d2, $d3, $unitPrice);
$suggestedOrderQty = $stockAnalysis['order_qty'];
$suggestedOrderCost = $stockAnalysis['order_cost'];
$coverageDateStr = '';
if ($suggestedOrderQty > 0) {
// Tooltip için tarih hesapla: Bugün + 3 ay
$targetDate = new \DateTime();
$targetDate->modify('+3 months');
$coverageDateStr = $targetDate->format('d.m.Y');
}
// 1. Tavsiye Edilen Stok Alımı (Tooltip ile)
$tooltipAttr = $suggestedOrderQty > 0 ? 'data-toggle="tooltip" title="' . $coverageDateStr . ' tarihine kadar yeterli"' : '';
$style = $suggestedOrderQty > 0 ? 'color: #e74a3b; font-size:1.1em; cursor:help;' : 'text-muted';
$valInfo = $suggestedOrderQty > 0 ? number_format($suggestedOrderQty, 0, ',', '.') : '-';
$item['suggestedOrderQty'] = [
'display' => '<span class="font-weight-bold" ' . $tooltipAttr . ' style="' . $style . '">' . $valInfo . '</span>',
'sort' => $suggestedOrderQty
];
// 2. Tavsiye Edilen Stok Maliyeti (Yeni Sütun)
$item['suggestedOrderCost'] = [
'display' => $suggestedOrderCost > 0 ? '<span class="text-danger">' . number_format($suggestedOrderCost, 2, ',', '.') . ' ' . $currencySymbol . '</span>' : '-',
'sort' => $suggestedOrderCost
];
// 3. Birim Maliyet (Yeni Sütun)
$item['unitPrice'] = [
'display' => number_format($unitPrice, 2, ',', '.') . ' ' . $currencySymbol,
'sort' => $unitPrice
];
$data[] = $item;
}
// Client-side DataTable için sadece data array döndürülüyor
return new JsonResponse([
'data' => $data
]);
}
/**
* AJAX endpoint for monthly stock purchase detail (used in budget planning info popup)
*/
#[Route('/admin/dashboard/stock-purchase-detail/{month}', name: 'admin_dashboard_stock_purchase_detail', methods: ['GET'])]
public function getStockPurchaseDetail(int $month, FinancialForecastingService $financialService): JsonResponse
{
// Validate month
if ($month < 1 || $month > 12) {
return new JsonResponse(['error' => 'Invalid month'], 400);
}
$detail = $financialService->getMonthlyStockPurchaseDetail(2025, 2026, $month, true);
return new JsonResponse($detail);
}
/**
* AJAX endpoint for 2026 Stock & Order Simulation data (for DataTables on Dashboard)
*/
#[Route('/admin/api/stock-simulation-data', name: 'app_dashboard_stock_simulation_data', methods: ['GET'])]
public function getStockSimulationData(\App\Service\Analysis\StrategicForecastingService $forecastingService): JsonResponse
{
// Generate forecast data
$forecastData = $forecastingService->generateForecast(2025, 2026);
$forecasts = $forecastData['product_forecasts'];
$data = [];
foreach ($forecasts as $pid => $product) {
$row = [
'product_code' => $product['code'],
'product_name' => $product['name'],
];
// Add monthly data
$totalSales2025 = 0;
for ($month = 1; $month <= 12; $month++) {
$plan = $product['plan'][$month] ?? [];
$demand = (int) ($plan['demand'] ?? 0);
$row['month_' . $month . '_stock'] = (int) ($plan['stock_end'] ?? 0);
$row['month_' . $month . '_demand'] = $demand;
$row['month_' . $month . '_order'] = (int) ($plan['order_qty'] ?? 0);
$row['month_' . $month . '_status'] = $plan['status'] ?? 'secure';
$totalSales2025 += $demand;
}
$row['total_sales_2025'] = $totalSales2025;
$data[] = $row;
}
return new JsonResponse(['data' => $data]);
}
/**
* AJAX endpoint for Yearly Plan Table data
* Veri Kaynakları:
* - Yıllık Satılan: product_monthly_stats.total_sales_qty (SUM for year)
* - Mevcut Stok: stock_info.total_quantity (SUM)
* - Birim Fiyat: product_monthly_stats.avg_cost_price (Ağırlıklı ortalama)
*/
#[Route('/admin/api/yearly-plan-data', name: 'app_dashboard_yearly_plan_data', methods: ['GET'])]
public function getYearlyPlanData(ProductMonthlyStatsRepository $statsRepository): JsonResponse
{
// 2025 yılı için tüm ürünlerin aylık istatistiklerini al
$allStats = $statsRepository->findBy(['year' => 2025]);
// Ürün bazlı verileri topla
$productData = [];
foreach ($allStats as $stat) {
$product = $stat->getProduct();
if ($product === null) {
continue;
}
$pId = $product->getId();
if (!isset($productData[$pId])) {
// Mevcut stok: StockInfo tablosundan
$currentStock = 0;
foreach ($product->getStockInfos() as $stockInfo) {
$currentStock += $stockInfo->getTotalQuantity();
}
$productData[$pId] = [
'id' => $pId,
'name' => $product->getName(),
'code' => $product->getCode(),
'currentStock' => (float) $currentStock,
'totalSales' => 0,
'totalCostSum' => 0,
'totalQtyForCost' => 0,
];
}
$m = $stat->getMonth();
if ($m >= 1 && $m <= 12) {
$qty = (float) $stat->getTotalSalesQty();
$avgCostPrice = (float) $stat->getAvgCostPrice();
$productData[$pId]['totalSales'] += $qty;
// Ağırlıklı ortalama maliyet hesabı için
if ($qty > 0 && $avgCostPrice > 0) {
$productData[$pId]['totalCostSum'] += ($qty * $avgCostPrice);
$productData[$pId]['totalQtyForCost'] += $qty;
}
}
}
// Hesaplamaları yap ve sonuç dizisini oluştur
$result = [];
foreach ($productData as $pId => $data) {
$yearlySales = $data['totalSales'];
$currentStock = $data['currentStock'];
// Birim Fiyat: Ağırlıklı ortalama
$unitPrice = 0;
if ($data['totalQtyForCost'] > 0) {
$unitPrice = $data['totalCostSum'] / $data['totalQtyForCost'];
}
// Hesaplamalar
$salesMinusStock = $yearlySales - $currentStock;
$orderQty = max(0, ceil($salesMinusStock));
$totalCost = $orderQty * $unitPrice;
$result[] = [
'id' => $pId,
'name' => $data['name'],
'code' => $data['code'],
'yearlySales' => ceil($yearlySales), // Yıllık Satılan
'currentStock' => $currentStock, // Mevcut Stok
'salesMinusStock' => ceil($salesMinusStock), // Satılan - Stok
'orderQty' => $orderQty, // Sipariş Miktarı
'unitPrice' => round($unitPrice, 2), // Birim Fiyat
'totalCost' => round($totalCost, 2), // Toplam
];
}
// Yıllık satışa göre sırala (çoktan aza)
usort($result, function ($a, $b) {
return $b['yearlySales'] <=> $a['yearlySales'];
});
return new JsonResponse(['data' => $result]);
}
/**
* AJAX endpoint for Monthly Plan Table data
* Seçilen ay için ürün bazlı satış ve sipariş planı
* Yıllık satışa göre sıralanır (en çoktan en aza)
*/
#[Route('/admin/api/monthly-plan-data/{month}', name: 'app_dashboard_monthly_plan_data', methods: ['GET'])]
public function getMonthlyPlanData(int $month, ProductMonthlyStatsRepository $statsRepository): JsonResponse
{
// Validate month
if ($month < 1 || $month > 12) {
return new JsonResponse(['error' => 'Invalid month'], 400);
}
// 2025 yılı için tüm ürünlerin aylık istatistiklerini al
$allStats = $statsRepository->findBy(['year' => 2025]);
// Ürün bazlı verileri topla
$productData = [];
foreach ($allStats as $stat) {
$product = $stat->getProduct();
if ($product === null) {
continue;
}
$pId = $product->getId();
if (!isset($productData[$pId])) {
// Mevcut stok: StockInfo tablosundan
$currentStock = 0;
foreach ($product->getStockInfos() as $stockInfo) {
$currentStock += $stockInfo->getTotalQuantity();
}
$productData[$pId] = [
'id' => $pId,
'name' => $product->getName(),
'code' => $product->getCode(),
'currentStock' => (float) $currentStock,
'yearlySales' => 0,
'monthlySales' => 0,
'monthlyUnitPrice' => 0,
];
}
$m = $stat->getMonth();
$qty = (float) $stat->getTotalSalesQty();
$avgCostPrice = (float) $stat->getAvgCostPrice();
// Yıllık toplam satış
if ($m >= 1 && $m <= 12) {
$productData[$pId]['yearlySales'] += $qty;
}
// Seçilen ay için satış
if ($m === $month) {
$productData[$pId]['monthlySales'] = $qty;
$productData[$pId]['monthlyUnitPrice'] = $avgCostPrice;
}
}
// Hesaplamaları yap ve sonuç dizisini oluştur
$result = [];
foreach ($productData as $pId => $data) {
$monthlySales = $data['monthlySales'];
$currentStock = $data['currentStock'];
$unitPrice = $data['monthlyUnitPrice'];
// Hesaplamalar
$stockMinusSales = $currentStock - $monthlySales;
$orderQty = max(0, ceil($monthlySales - $currentStock));
$totalCost = $orderQty * $unitPrice;
$result[] = [
'id' => $pId,
'name' => $data['name'],
'code' => $data['code'],
'currentStock' => $currentStock, // Mevcut Stok
'monthlySales' => ceil($monthlySales), // Aylık Satılan
'stockMinusSales' => ceil($stockMinusSales), // Stok - Satılan
'unitPrice' => round($unitPrice, 2), // Birim Fiyat
'orderQty' => $orderQty, // Sipariş Miktarı
'totalCost' => round($totalCost, 2), // Toplam
'yearlySales' => ceil($data['yearlySales']), // Sıralama için
];
}
// Yıllık satışa göre sırala (çoktan aza)
usort($result, function ($a, $b) {
return $b['yearlySales'] <=> $a['yearlySales'];
});
return new JsonResponse(['data' => $result]);
}
#[Route('/monthly-plan/{month}', name: 'app_dashboard_monthly_plan_data', methods: ['GET'])]
public function getMonthlyOrderPlan(int $month, ProductMonthlyStatsRepository $statsRepository, \Symfony\Component\HttpFoundation\Request $request): JsonResponse
{
$filterName = $request->query->get('name');
$filterCodes = $request->query->all()['codes'] ?? [];
$filterMeasurements = $request->query->all()['measurements'] ?? [];
$filterUnits = $request->query->all()['units'] ?? [];
// 1. Fetch all 2025 stats
$allStats2025 = $statsRepository->findBy(['year' => 2025]);
// 2. Group by Product
$productData = [];
foreach ($allStats2025 as $stat) {
$product = $stat->getProduct();
// Skip if product is null OR if it has no measurement unit
if ($product === null || $product->getMeasurementUnit() === null) {
continue;
}
$code = $product->getCode();
$key = $code ? strtoupper(trim($code)) : $product->getId();
if (!isset($productData[$key])) {
$productData[$key] = [
'name' => $product->getName(),
'code' => $product->getCode(),
'currentStock' => 0,
'total2025' => 0,
'monthlySales' => 0, // Sales for the requested month
'totalCostSum' => 0,
'totalQtyForCost' => 0,
'seen_pids' => [],
'measurement' => $product->getMeasurement() ? $product->getMeasurement()->getMeasurement() : '',
'measurementId' => $product->getMeasurement() ? $product->getMeasurement()->getId() : null,
'unitId' => $product->getMeasurementUnit() ? $product->getMeasurementUnit()->getId() : null,
'unitSymbol' => $product->getMeasurementUnit() ? $product->getMeasurementUnit()->getSymbol() : ''
];
}
$pId = $product->getId();
if (!in_array($pId, $productData[$key]['seen_pids'])) {
$thisStock = array_reduce($product->getStockInfos()->toArray(), function ($sum, $item) {
return $sum + $item->getTotalQuantity();
}, 0);
$productData[$key]['currentStock'] += $thisStock;
$productData[$key]['seen_pids'][] = $pId;
}
$m = $stat->getMonth();
$qty = (float) $stat->getTotalSalesQty();
$avgCost = (float) $stat->getAvgCostPrice();
// Total 2025 Sales
if ($m >= 1 && $m <= 12) {
$productData[$key]['total2025'] += $qty;
// For Unit Cost Calculation
if ($qty > 0 && $avgCost > 0) {
$productData[$key]['totalCostSum'] += ($qty * $avgCost);
$productData[$key]['totalQtyForCost'] += $qty;
}
}
// Month specific sales
if ($m == $month) {
$productData[$key]['monthlySales'] += $qty;
}
}
// 3. Format result and Calculate Totals
$result = [];
$totalS1Order = 0;
$totalS1Cost = 0;
$totalS2Order = 0;
$totalS2Cost = 0;
foreach ($productData as $key => $data) {
// Server-side filtering
if ($filterName && stripos($data['name'], $filterName) === false)
continue;
if (!empty($filterCodes) && !in_array($data['code'], $filterCodes))
continue;
if (!empty($filterMeasurements) && !in_array($data['measurementId'], $filterMeasurements))
continue;
if (!empty($filterUnits) && !in_array($data['unitId'], $filterUnits))
continue;
// Unit Price (Weighted Average)
$unitPrice = 0;
if ($data['totalQtyForCost'] > 0) {
$unitPrice = $data['totalCostSum'] / $data['totalQtyForCost'];
}
$currentStock = $data['currentStock'];
$monthSales = ceil($data['monthlySales']);
$total2025 = ceil($data['total2025']);
// Scenario 1: Equal Distribution (Stock / 12)
$allocatedEqual = $currentStock / 12;
$orderEqual = 0;
// Order amount is difference between Demand (monthSales) and Allocated Stock
// 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."
// So: monthSales - allocatedEqual
if ($monthSales > $allocatedEqual) {
$orderEqual = $monthSales - $allocatedEqual;
}
$costEqual = $orderEqual * $unitPrice;
// Scenario 2: Weighted Distribution (Stock * (MonthSale / YearSale))
$allocatedWeighted = 0;
if ($total2025 > 0) {
$ratio = $monthSales / $total2025;
$allocatedWeighted = $currentStock * $ratio;
}
$orderWeighted = 0;
if ($monthSales > $allocatedWeighted) {
$orderWeighted = $monthSales - $allocatedWeighted;
}
$costWeighted = $orderWeighted * $unitPrice;
// Add to totals
$totalS1Order += ceil($orderEqual);
$totalS1Cost += $costEqual;
$totalS2Order += ceil($orderWeighted);
$totalS2Cost += $costWeighted;
$result[] = [
'name' => $data['name'],
'code' => $data['code'],
'currentStock' => $currentStock,
'sales2025' => $total2025,
'monthSales' => $monthSales, // Demand
'unitPrice' => round($unitPrice, 2),
// Scenario 1
'allocatedEqual' => floor($allocatedEqual),
'orderEqual' => ceil($orderEqual),
'costEqual' => round($costEqual, 2),
// Scenario 2
'allocatedWeighted' => floor($allocatedWeighted),
'orderWeighted' => ceil($orderWeighted),
'costWeighted' => round($costWeighted, 2),
'measurement' => $data['measurement'],
'measurementId' => $data['measurementId'],
'unitId' => $data['unitId'],
'unitSymbol' => $data['unitSymbol']
];
}
// Sort by Monthly Sales Descending (Primary focus) or Order Cost
usort($result, function ($a, $b) {
return $b['monthSales'] <=> $a['monthSales'];
});
return new JsonResponse([
'data' => $result,
'totals' => [
's1Order' => $totalS1Order,
's1Cost' => round($totalS1Cost, 2),
's2Order' => $totalS2Order,
's2Cost' => round($totalS2Cost, 2)
]
]);
}
}