transaksiRepo = $transaksiRepo; $this->helper = $helper; } public function getRingkasan(string $filter, int $page) { $cacheKey = "laporan_ringkasan_{$filter}_page_{$page}"; return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filter, $page) { $allSalesNames = $this->getAllSalesNames(); if ($filter === 'hari') { return $this->processLaporanHarian($allSalesNames, $page, true); } return $this->processLaporanBulanan($allSalesNames, $page, true); }); } /** * Get paginated sales detail aggregated by product. * * @param array $params Filter parameters (tanggal, sales_id, nampan_id, nama_pembeli, page, per_page) * @return array Report data structure * @throws \Exception */ public function getDetailPerProduk(array $params) { $startDate = Carbon::parse($params['start_date'])->startOfDay(); $endDate = Carbon::parse($params['end_date'])->endOfDay(); // TAMBAH: Validasi range max 30 hari (backup request) if ($startDate->diffInDays($endDate) > 30) { throw new \InvalidArgumentException('Interval tanggal maksimal 30 hari.'); } $page = $params['page'] ?? 1; $perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE; // --- Step 1: Totals --- $totalsQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); // FIXED: Call benar $this->applyFilters($totalsQuery, $params); $totalsResult = $totalsQuery->select( DB::raw('COUNT(item_transaksis.id) as total_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as total_berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan') )->first(); $rekapInterval = [ 'total_item_terjual' => (int) $totalsResult->total_item_terjual, 'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual), 'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan), ]; // --- Step 2: Subquery --- $salesSubQuery = $this->buildBaseItemQueryForRange($startDate, $endDate) ->select( 'produks.id as id_produk', DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan') ) ->groupBy('produks.id'); $this->applyFilters($salesSubQuery, $params); // --- Step 3: Paginated products --- $semuaProdukPaginated = Produk::select( 'produks.id', 'produks.nama as nama_produk', 'sales_data.jumlah_item_terjual', 'sales_data.berat_terjual', 'sales_data.pendapatan' ) ->leftJoinSub($salesSubQuery, 'sales_data', function ($join) { $join->on('produks.id', '=', 'sales_data.id_produk'); }) ->orderBy('produks.nama') ->paginate($perPage, ['*'], 'page', $page); // --- Step 4: Map & filter --- $detailItem = $semuaProdukPaginated->map(function ($item) { return [ 'nama_produk' => $item->nama_produk, 'jumlah_item_terjual' => $item->jumlah_item_terjual ? (int) $item->jumlah_item_terjual : 0, 'berat_terjual' => $item->berat_terjual ? $this->helper->formatWeight($item->berat_terjual) : '-', 'pendapatan' => $item->pendapatan ? $this->helper->formatCurrency($item->pendapatan) : '-', ]; })->filter(function ($item) { return $item['jumlah_item_terjual'] > 0; }); $paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator( $detailItem->forPage($page, $perPage), $detailItem->count(), // FIXED: Total dari filtered $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); // --- Step 5: Response --- $filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params); return [ 'filter' => $filterInfo, 'rekap_interval' => $rekapInterval, 'produk' => $paginatedFiltered->getCollection(), 'pagination' => $this->helper->buildPaginationInfo($paginatedFiltered), // FIXED: Dari filtered ]; } private function buildBaseItemQueryForRange(Carbon $startDate, Carbon $endDate) { return ItemTransaksi::query() ->join('produks', 'item_transaksis.id_produk', '=', 'produks.id') ->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id') ->whereBetween('transaksis.created_at', [$startDate, $endDate]); } public function getDetailPerNampan(array $params) { $startDate = Carbon::parse($params['start_date'])->startOfDay(); $endDate = Carbon::parse($params['end_date'])->endOfDay(); if ($startDate->diffInDays($endDate) > 30) { throw new \InvalidArgumentException('Interval tanggal maksimal 30 hari.'); } $page = $params['page'] ?? 1; $perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE; $nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); // FIXED: Range $this->applyNampanFilters($nampanTerjualQuery, $params); $nampanTerjual = $nampanTerjualQuery ->select( DB::raw('COALESCE(item_transaksis.posisi_asal, "Brankas") as nama_nampan'), DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan') ) ->groupBy('nama_nampan') ->get() ->keyBy('nama_nampan'); // FIXED: calculateTotals sum raw (bukan formatted string) $nampanTerjualRaw = $nampanTerjual->map(function ($item) { return [ 'jumlah_item_terjual' => (int) $item->jumlah_item_terjual, 'berat_terjual' => $item->berat_terjual, 'pendapatan' => $item->pendapatan, ]; }); $totals = $this->helper->calculateTotals($nampanTerjualRaw); $semuaNampanPaginated = $this->helper->getAllNampanWithPagination($page, $perPage); $detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual) ->filter(function ($item) { return $item['jumlah_item_terjual'] > 0; // FIXED: Int compare, no DEFAULT_DISPLAY check }); $paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator( $detailItem->forPage($page, $perPage), $detailItem->count(), $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); $filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params); // FIXED: Range helper return [ 'filter' => $filterInfo, 'rekap_interval' => $totals, // FIXED: Rename 'nampan' => $paginatedFiltered->getCollection(), 'pagination' => $this->helper->buildPaginationInfo($paginatedFiltered), ]; } public function exportRingkasan(array $params) { $filter = $params['filter']; $format = $params['format']; $page = $params['page'] ?? 1; $allSalesNames = $this->getAllSalesNames(); if ($filter === 'hari') { $data = $this->processLaporanHarian($allSalesNames, $page, true); } else { $data = $this->processLaporanBulanan($allSalesNames, $page, true); } $fileName = "laporan_ringkasan_{$filter}_" . Carbon::now()->format('Ymd') . ".{$format}"; if ($format === 'pdf') { $viewData = method_exists($data, 'items') ? $data->items() : $data; $pdf = PDF::loadView('exports.ringkasan_pdf', [ 'data' => $viewData, 'filter' => $filter ]); $pdf->setPaper('a4', 'potrait'); return $pdf->download($fileName); } return Excel::download(new RingkasanExport($data, $page), $fileName); } public function exportPerProduk(array $params) { $format = $params['format']; $allParams = $params; unset($allParams['page'], $allParams['per_page']); $data = $this->getDetailPerProdukForExport($allParams); $startDate = Carbon::parse($params['start_date'])->format('Ymd'); $endDate = Carbon::parse($params['end_date'])->format('Ymd'); $fileName = "laporan_per_produk_{$startDate}_to_{$endDate}.{$format}"; // FIXED: Range filename if ($format === 'pdf') { $pdf = PDF::loadView('exports.perproduk_pdf', [ 'data' => $data, 'title' => 'Laporan Detail Per Produk' ]); $pdf->setPaper('a4', 'portrait'); // FIXED: Typo 'potrait' return $pdf->download($fileName); } return Excel::download(new DetailProdukExport($data), $fileName); } public function exportPerNampan(array $params) { $format = $params['format']; $allParams = $params; unset($allParams['page'], $allParams['per_page']); $data = $this->getDetailPerNampanForExport($allParams); $startDate = Carbon::parse($params['start_date'])->format('Ymd'); $endDate = Carbon::parse($params['end_date'])->format('Ymd'); $fileName = "laporan_per_nampan_{$startDate}_to_{$endDate}.{$format}"; // FIXED: Range if ($format === 'pdf') { $pdf = PDF::loadView('exports.pernampan_pdf', [ 'data' => $data, 'title' => 'Laporan Detail Per Nampan' ]); $pdf->setPaper('a4', 'portrait'); return $pdf->download($fileName); } return Excel::download(new DetailNampanExport($data), $fileName); } private function getDetailPerProdukForExport(array $params) { $startDate = Carbon::parse($params['start_date'])->startOfDay(); $endDate = Carbon::parse($params['end_date'])->endOfDay(); $produkTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); $this->applyFilters($produkTerjualQuery, $params); $produkTerjual = $produkTerjualQuery ->select( 'produks.id as id_produk', 'produks.nama as nama_produk', DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan') ) ->groupBy('produks.id', 'produks.nama') ->get() ->keyBy('id_produk'); // FIXED: calculateTotals sum raw $produkTerjualRaw = $produkTerjual->map(function ($item) { return [ 'jumlah_item_terjual' => (int) $item->jumlah_item_terjual, 'berat_terjual' => $item->berat_terjual, 'pendapatan' => $item->pendapatan, ]; }); $totals = $this->helper->calculateTotals($produkTerjualRaw); $semuaProduk = Produk::select('id', 'nama')->orderBy('nama')->get(); $detailItem = collect($semuaProduk)->map(function ($item) use ($produkTerjual) { if ($produkTerjual->has($item->id)) { $dataTerjual = $produkTerjual->get($item->id); return [ 'nama_produk' => $item->nama, 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, 'berat_terjual' => $this->helper->formatWeight($dataTerjual->berat_terjual), 'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan), ]; } return null; // Akan difilter })->filter(); // FIXED: Filter null/kosong $filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params); return [ 'filter' => $filterInfo, 'rekap_interval' => $totals, // FIXED: Rename 'produk' => $detailItem->values(), ]; } private function getDetailPerNampanForExport(array $params) { $startDate = Carbon::parse($params['start_date'])->startOfDay(); $endDate = Carbon::parse($params['end_date'])->endOfDay(); $nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); $this->applyNampanFilters($nampanTerjualQuery, $params); $nampanTerjual = $nampanTerjualQuery ->select( 'item_transaksis.posisi_asal as posisi_asal', DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan') ) ->groupBy('item_transaksis.posisi_asal') ->get() ->keyBy('posisi_asal'); // FIXED: Sum raw $nampanTerjualRaw = $nampanTerjual->map(function ($item) { return [ 'jumlah_item_terjual' => (int) $item->jumlah_item_terjual, 'berat_terjual' => $item->berat_terjual, 'pendapatan' => $item->pendapatan, ]; }); $totals = $this->helper->calculateTotals($nampanTerjualRaw); $semuaPosisi = DB::table('item_transaksis') ->whereBetween('created_at', [$startDate, $endDate]) // FIXED: Filter posisi di range ->select('posisi_asal') ->distinct() ->pluck('posisi_asal') ->sort() ->values(); $detailItem = $semuaPosisi->map(function ($posisi) use ($nampanTerjual) { if ($nampanTerjual->has($posisi)) { $dataTerjual = $nampanTerjual->get($posisi); return [ 'nama_nampan' => $posisi, 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, 'berat_terjual' => $this->helper->formatWeight($dataTerjual->berat_terjual), 'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan), ]; } return null; // Filter out })->filter(); $filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params); // FIXED: Range return [ 'filter' => $filterInfo, 'rekap_interval' => $totals, 'nampan' => $detailItem->values(), ]; } private function getAllSalesNames(): Collection { return Cache::remember('all_sales_names', self::CACHE_TTL, function () { return Transaksi::select('nama_sales')->distinct()->pluck('nama_sales'); }); } private function processLaporanHarian(Collection $allSalesNames, int $page = 1, bool $limitPagination = true) { return $this->transaksiRepo->processLaporanHarian($allSalesNames, $page, $limitPagination); } private function processLaporanBulanan(Collection $allSalesNames, int $page = 1, bool $limitPagination = true) { return $this->transaksiRepo->processLaporanBulanan($allSalesNames, $page, $limitPagination); } private function applyFilters($query, array $params): void { if (!empty($params['sales_id'])) { $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') ->where('sales.id', $params['sales_id']); } if (isset($params['nampan_id'])) { $nampanId = (int) $params['nampan_id']; if ($nampanId === -1) { $query->where('item_transaksis.posisi_asal', 'Brankas'); } elseif ($nampanId > 0) { // FIXED: >0 join, 0 skip (all) $query->join('nampans', function ($join) use ($nampanId) { $join->on('item_transaksis.posisi_asal', '=', 'nampans.nama') ->where('nampans.id', $nampanId); }); } } if (!empty($params['nama_pembeli'])) { $query->where('transaksis.nama_pembeli', 'like', "%{$params['nama_pembeli']}%"); } } private function applyNampanFilters($query, array $params): void { if (!empty($params['sales_id'])) { $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') ->where('sales.id', $params['sales_id']); } if (!empty($params['produk_id'])) { $query->where('produks.id', $params['produk_id']); } if (!empty($params['nama_pembeli'])) { $query->where('transaksis.nama_pembeli', 'like', "%{$params['nama_pembeli']}%"); } } }