diff --git a/app/Exports/DetailNampanExport.php b/app/Exports/DetailNampanExport.php index bfdeb05..fad8581 100644 --- a/app/Exports/DetailNampanExport.php +++ b/app/Exports/DetailNampanExport.php @@ -11,29 +11,36 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, WithStyles { private $data; - private $page; - public function __construct($data, $page = 1) + public function __construct($data) { $this->data = $data; - $this->page = $page; } public function collection() { $collection = collect(); - + + // Add individual nampan data if (isset($this->data['nampan'])) { foreach ($this->data['nampan'] as $item) { $collection->push([ - 'Nama Nampan' => $item['nama_nampan'], - 'Jumlah Item Terjual' => $item['jumlah_item_terjual'], - 'Berat Terjual' => $item['berat_terjual'], - 'Pendapatan' => $item['pendapatan'], + $item['nama_nampan'], + $item['jumlah_item_terjual'], + $item['berat_terjual'], + $item['pendapatan'], ]); } } - + if (isset($this->data['rekap_harian'])) { + $rekap = $this->data['rekap_harian']; + $collection->push([ + 'REKAP TOTAL', + $rekap['total_item_terjual'], + $rekap['total_berat_terjual'], + $rekap['total_pendapatan'], + ]); + } return $collection; } @@ -41,7 +48,7 @@ class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, Wit { return [ 'Nama Nampan', - 'Jumlah Item Terjual', + 'Jumlah Item Terjual', 'Berat Terjual', 'Pendapatan' ]; @@ -51,13 +58,26 @@ class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, Wit { $filterInfo = $this->data['filter'] ?? []; $tanggal = $filterInfo['tanggal'] ?? 'Unknown'; - return "Detail Nampan {$tanggal} - Hal {$this->page}"; + return "Detail Nampan {$tanggal}"; } public function styles(Worksheet $sheet) { - return [ - 1 => ['font' => ['bold' => true]], + $styles = [ + 1 => ['font' => ['bold' => true]], // Header row ]; + + // Style for recap row if exists + if (isset($this->data['rekap_harian'])) { + $styles[2] = [ + 'font' => ['bold' => true], + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['argb' => 'FFE2E3E5'], + ], + ]; + } + + return $styles; } -} \ No newline at end of file +} diff --git a/app/Exports/DetailProdukExport.php b/app/Exports/DetailProdukExport.php index b44d463..56cbcdf 100644 --- a/app/Exports/DetailProdukExport.php +++ b/app/Exports/DetailProdukExport.php @@ -11,25 +11,38 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class DetailProdukExport implements FromCollection, WithHeadings, WithTitle, WithStyles { private $data; - private $page; - public function __construct($data, $page = 1) + public function __construct($data) { $this->data = $data; - $this->page = $page; } public function collection() { $collection = collect(); + // Add summary row first + if (isset($this->data['rekap_harian'])) { + $rekap = $this->data['rekap_harian']; + $collection->push([ + 'REKAP TOTAL', + $rekap['total_item_terjual'], + $rekap['total_berat_terjual'], + $rekap['total_pendapatan'], + ]); + + // Add empty row separator + $collection->push(['', '', '', '']); + } + + // Add individual produk data if (isset($this->data['produk'])) { foreach ($this->data['produk'] as $item) { $collection->push([ - 'Nama Produk' => $item['nama_produk'], - 'Jumlah Item Terjual' => $item['jumlah_item_terjual'], - 'Berat Terjual' => $item['berat_terjual'], - 'Pendapatan' => $item['pendapatan'], + $item['nama_produk'], + $item['jumlah_item_terjual'], + $item['berat_terjual'], + $item['pendapatan'], ]); } } @@ -51,13 +64,26 @@ class DetailProdukExport implements FromCollection, WithHeadings, WithTitle, Wit { $filterInfo = $this->data['filter'] ?? []; $tanggal = $filterInfo['tanggal'] ?? 'Unknown'; - return "Detail Produk {$tanggal} - Hal {$this->page}"; + return "Detail Produk {$tanggal}"; } public function styles(Worksheet $sheet) { - return [ - 1 => ['font' => ['bold' => true]], + $styles = [ + 1 => ['font' => ['bold' => true]], // Header row ]; + + // Style for recap row if exists + if (isset($this->data['rekap_harian'])) { + $styles[2] = [ + 'font' => ['bold' => true], + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['argb' => 'FFE2E3E5'], + ], + ]; + } + + return $styles; } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/LaporanController.php b/app/Http/Controllers/LaporanController.php index bc6b77c..98939cd 100644 --- a/app/Http/Controllers/LaporanController.php +++ b/app/Http/Controllers/LaporanController.php @@ -51,7 +51,7 @@ class LaporanController extends Controller return response()->json($data); } catch (\Exception $e) { - Log::error('Error in detailPerProduk method: ' . $e->getMessage()); + Log::error('Error in detail PerProduk method: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat mengambil data produk'], 500); } } @@ -84,4 +84,40 @@ class LaporanController extends Controller return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500); } } + + public function exportDetailNampan(Request $request) + { + try { + return $this->laporanService->exportPerNampan($request->validate([ + 'tanggal' => 'nullable|string', + 'sales_id' => 'nullable|integer|exists:sales,id', + 'produk_id' => 'nullable|integer|exists:produk,id', + 'nama_pembeli' => 'nullable|string|max:255', + 'format' => 'required|string|in:pdf,xlsx,csv', + 'page' => 'nullable|integer|min:1', + ])); + + } catch (\Exception $e) { + Log::error('Error in exprot per nampan method: ' . $e->getMessage()); + return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500); + } + } + + public function exportDetailProduk(Request $request) + { + try { + return $this->laporanService->exportPerProduk($request->validate([ + 'tanggal' => 'nullable|string', + 'sales_id' => 'nullable|integer|exists:sales,id', + 'nampan_id' => 'nullable|integer|exists:nampan,id', + 'nama_pembeli' => 'nullable|string|max:255', + 'format' => 'required|string|in:pdf,xlsx,csv', + 'page' => 'nullable|integer|min:1', + ])); + + } catch (\Exception $e) { + Log::error('Error in exprot per nampan method: ' . $e->getMessage()); + return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500); + } + } } diff --git a/app/Services/LaporanService.php b/app/Services/LaporanService.php index 80eff1c..ff9f812 100644 --- a/app/Services/LaporanService.php +++ b/app/Services/LaporanService.php @@ -41,58 +41,91 @@ class LaporanService 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) { $tanggal = Carbon::parse($params['tanggal']); $page = $params['page'] ?? 1; $perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE; - // Validasi nampan_id jika ada - if (isset($params['nampan_id']) && $params['nampan_id'] != 0) { - if (!Nampan::where('id', $params['nampan_id'])->exists()) { - throw new \Exception('Nampan tidak ditemukan'); - } - } + // --- Step 1: Calculate overall totals for all filtered items --- + // We need a separate query for totals that is not affected by pagination. + $totalsQuery = $this->buildBaseItemQuery($tanggal); + $this->applyFilters($totalsQuery, $params); - $produkTerjualQuery = $this->buildBaseItemQuery($tanggal); - $this->applyFilters($produkTerjualQuery, $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(); - $produkTerjual = $produkTerjualQuery + $rekapHarian = [ + 'total_item_terjual' => (int) $totalsResult->total_item_terjual, + 'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual), // Assuming formatting helper + 'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan), // Assuming formatting helper + ]; + + // --- Step 2: Build the filtered sales data subquery --- + $salesSubQuery = $this->buildBaseItemQuery($tanggal) ->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'); + ->groupBy('produks.id'); + // Apply filters to the subquery + $this->applyFilters($salesSubQuery, $params); - $totals = $this->helper->calculateTotals($produkTerjual); - $semuaProdukPaginated = Produk::select('id', 'nama') - ->orderBy('nama') + // --- Step 3: Fetch paginated products and LEFT JOIN the sales data subquery --- + $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); - - $detailItem = $this->helper->mapProductsWithSalesData($semuaProdukPaginated, $produkTerjual); + + // --- Step 4: Map results for final presentation --- + $detailItem = $semuaProdukPaginated->map(function ($item) { + return [ + 'nama_produk' => $item->nama_produk, + 'jumlah_item_terjual' => $item->jumlah_item_terjual ? (int) $item->jumlah_item_terjual : 0, // Use 0 or default display value + 'berat_terjual' => $item->berat_terjual ? $this->helper->formatWeight($item->berat_terjual) : '-', + 'pendapatan' => $item->pendapatan ? $this->helper->formatCurrency($item->pendapatan) : '-', + ]; + }); + + // --- Step 5: Assemble final response --- $filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params); return [ 'filter' => $filterInfo, - 'rekap_harian' => $totals, - 'produk' => $detailItem->values(), + 'rekap_harian' => $rekapHarian, + 'produk' => $detailItem, 'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated), ]; } @@ -137,11 +170,11 @@ class LaporanService $filter = $params['filter']; $format = $params['format']; $page = $params['page'] ?? 1; - + $allSalesNames = $this->getAllSalesNames(); - + if ($filter === 'hari') { - // Tar kalau mau ubah eksport laporan setiap hari, param ke-3 jadiin false #Bagas + // Tar kalau mau ubah eksport laporan setiap hari, param ke-3 jadiin false #Bagas $data = $this->processLaporanHarian($allSalesNames, $page, true); } else { $data = $this->processLaporanBulanan($allSalesNames, $page, true); @@ -156,13 +189,175 @@ class LaporanService 'data' => $viewData, 'filter' => $filter ]); - $pdf->setPaper('a4', 'landscape'); + $pdf->setPaper('a4', 'potrait'); return $pdf->download($fileName); } return Excel::download(new RingkasanExport($data, $page), $fileName); } + // Method baru untuk export per produk + public function exportPerProduk(array $params) + { + $tanggal = $params['tanggal']; + $format = $params['format']; + + // Get all data tanpa pagination karena untuk export + $allParams = $params; + unset($allParams['page'], $allParams['per_page']); + + // Get data dengan semua produk (tanpa pagination) + $data = $this->getDetailPerProdukForExport($allParams); + + $fileName = "laporan_per_produk_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}"; + + if ($format === 'pdf') { + $pdf = PDF::loadView('exports.perproduk_pdf', [ + 'data' => $data, + 'title' => 'Laporan Detail Per Produk' + ]); + $pdf->setPaper('a4', 'potrait'); + return $pdf->download($fileName); + } + + return Excel::download(new DetailProdukExport($data), $fileName); + } + + public function exportPerNampan(array $params) + { + $tanggal = $params['tanggal']; + $format = $params['format']; + + // Get all data tanpa pagination karena untuk export + $allParams = $params; + unset($allParams['page'], $allParams['per_page']); + + // Get data dengan semua nampan (tanpa pagination) + $data = $this->getDetailPerNampanForExport($allParams); + + $fileName = "laporan_per_nampan_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}"; + + if ($format === 'pdf') { + $pdf = PDF::loadView('exports.pernampan_pdf', [ + 'data' => $data, + 'title' => 'Laporan Detail Per Nampan' + ]); + $pdf->setPaper('a4', 'potrait'); + return $pdf->download($fileName); + } + + return Excel::download(new DetailNampanExport($data), $fileName); + } + + // Helper method untuk get data produk tanpa pagination (untuk export) + private function getDetailPerProdukForExport(array $params) + { + $tanggal = Carbon::parse($params['tanggal']); + + $produkTerjualQuery = $this->buildBaseItemQuery($tanggal); + $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'); + + $totals = $this->helper->calculateTotals($produkTerjual); + + // Get all products without pagination + $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 [ + 'nama_produk' => $item->nama, + 'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY, + 'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY, + 'pendapatan' => LaporanHelper::DEFAULT_DISPLAY, + ]; + }); + + $filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params); + + return [ + 'filter' => $filterInfo, + 'rekap_harian' => $totals, + 'produk' => $detailItem->values(), + ]; + } + + // Helper method untuk get data nampan tanpa pagination (untuk export) + private function getDetailPerNampanForExport(array $params) + { + $tanggal = Carbon::parse($params['tanggal']); + + $nampanTerjualQuery = $this->buildBaseItemQuery($tanggal); + $this->applyNampanFilters($nampanTerjualQuery, $params); + + $nampanTerjual = $nampanTerjualQuery + ->leftJoin('nampans', 'items.id_nampan', '=', 'nampans.id') + ->select( + DB::raw('COALESCE(items.id_nampan, 0) as id_nampan'), + DB::raw('COALESCE(nampans.nama, "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('id_nampan', 'nama_nampan') + ->get() + ->keyBy('id_nampan'); + + $totals = $this->helper->calculateTotals($nampanTerjual); + + // Get all nampan without pagination + $semuaNampan = Nampan::select('id', 'nama')->orderBy('nama')->get(); + $brankasEntry = (object) ['id' => 0, 'nama' => 'Brankas']; + $semuaNampanCollection = $semuaNampan->prepend($brankasEntry); + + $detailItem = $semuaNampanCollection->map(function ($item) use ($nampanTerjual) { + if ($nampanTerjual->has($item->id)) { + $dataTerjual = $nampanTerjual->get($item->id); + return [ + 'nama_nampan' => $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 [ + 'nama_nampan' => $item->nama, + 'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY, + 'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY, + 'pendapatan' => LaporanHelper::DEFAULT_DISPLAY, + ]; + }); + + $filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params); + + return [ + 'filter' => $filterInfo, + 'rekap_harian' => $totals, + 'nampan' => $detailItem->values(), + ]; + } + private function getAllSalesNames(): Collection { return Cache::remember('all_sales_names', self::CACHE_TTL, function () { diff --git a/resources/js/components/DetailPerNampan.vue b/resources/js/components/DetailPerNampan.vue index 1687899..f9d872b 100644 --- a/resources/js/components/DetailPerNampan.vue +++ b/resources/js/components/DetailPerNampan.vue @@ -1,7 +1,7 @@