From f71fabdc902974c0af6c5e1362953fbc84539cab Mon Sep 17 00:00:00 2001 From: Baghaztra Date: Fri, 19 Sep 2025 13:26:53 +0700 Subject: [PATCH] [Update] Filter interval laporan --- app/Helpers/LaporanHelper.php | 23 +- app/Http/Controllers/LaporanController.php | 24 +- app/Http/Requests/DetailLaporanRequest.php | 7 +- app/Services/LaporanService.php | 169 +++++++------- database/seeders/DatabaseSeeder.php | 4 +- resources/js/components/DatePicker.vue | 206 ++++++++++++++++++ resources/js/components/DetailPerNampan.vue | 96 +++++--- resources/js/components/DetailPerProduk.vue | 115 ++++++---- .../views/exports/pernampan_pdf.blade.php | 2 +- .../views/exports/perproduk_pdf.blade.php | 2 +- 10 files changed, 473 insertions(+), 175 deletions(-) create mode 100644 resources/js/components/DatePicker.vue diff --git a/app/Helpers/LaporanHelper.php b/app/Helpers/LaporanHelper.php index 4632d09..7d44122 100644 --- a/app/Helpers/LaporanHelper.php +++ b/app/Helpers/LaporanHelper.php @@ -18,9 +18,10 @@ class LaporanHelper public function calculateTotals(Collection $data): array { - $totalPendapatan = $data->sum('pendapatan'); - $totalItemTerjual = $data->sum('jumlah_item_terjual'); - $totalBeratTerjual = $data->sum('berat_terjual'); + // Asumsi $data punya raw numeric (int/float) + $totalPendapatan = $data->sum('pendapatan'); // Raw float + $totalItemTerjual = $data->sum('jumlah_item_terjual'); // Int + $totalBeratTerjual = $data->sum('berat_terjual'); // Float return [ 'total_item_terjual' => $totalItemTerjual, @@ -94,12 +95,12 @@ class LaporanHelper }); } - public function buildProdukFilterInfo(Carbon $carbonDate, array $params): array + public function buildProdukFilterInfo(Carbon $startDate, Carbon $endDate, array $params): array { $filterInfo = [ - 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), + 'periode' => "{$startDate->isoFormat('D MMMM Y')} - {$endDate->isoFormat('D MMMM Y')}", 'nama_sales' => null, - 'nampan' => null, + 'nampan' => null, // Default null 'nama_pembeli' => $params['nama_pembeli'] ?? null, ]; @@ -109,21 +110,23 @@ class LaporanHelper } if (isset($params['nampan_id'])) { - if ($params['nampan_id'] == 0) { + if ($params['nampan_id'] === -1) { $filterInfo['nampan'] = 'Brankas'; - } else { + } elseif ($params['nampan_id'] > 0) { $nampan = Nampan::find($params['nampan_id']); $filterInfo['nampan'] = $nampan?->nama; + } else { // 0: Semua + $filterInfo['nampan'] = 'Semua Nampan'; } } return $filterInfo; } - public function buildNampanFilterInfo(Carbon $carbonDate, array $params): array + public function buildNampanFilterInfo(Carbon $startDate, Carbon $endDate, array $params): array { $filterInfo = [ - 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), + 'periode' => "{$startDate->isoFormat('D MMMM Y')} - {$endDate->isoFormat('D MMMM Y')}", // FIXED: Range 'nama_sales' => null, 'produk' => null, 'nama_pembeli' => $params['nama_pembeli'] ?? null, diff --git a/app/Http/Controllers/LaporanController.php b/app/Http/Controllers/LaporanController.php index 0f0a4da..3bc6d24 100644 --- a/app/Http/Controllers/LaporanController.php +++ b/app/Http/Controllers/LaporanController.php @@ -88,17 +88,18 @@ class LaporanController extends Controller public function exportDetailNampan(Request $request) { try { - return $this->laporanService->exportPerNampan($request->validate([ - 'tanggal' => 'required|string', + $validated = $request->validate([ + 'start_date' => 'required|date_format:Y-m-d', + 'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date', 'format' => 'required|string|in:pdf,xlsx,csv', 'page' => 'required|integer|min:1', 'sales_id' => 'nullable|integer|exists:sales,id', 'produk_id' => 'nullable|integer|exists:produks,id', 'nama_pembeli' => 'nullable|string|max:255', - ])); - + ]); + return $this->laporanService->exportPerNampan($validated); } catch (\Exception $e) { - Log::error('Error in exprot per nampan method: ' . $e->getMessage()); + Log::error('Error in export per nampan: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500); } } @@ -106,17 +107,18 @@ class LaporanController extends Controller public function exportDetailProduk(Request $request) { try { - return $this->laporanService->exportPerProduk($request->validate([ - 'tanggal' => 'required|string', + $validated = $request->validate([ + 'start_date' => 'required|date_format:Y-m-d', + 'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date', 'format' => 'required|string|in:pdf,xlsx,csv', 'page' => 'required|integer|min:1', 'sales_id' => 'nullable|integer|exists:sales,id', - 'nampan_id' => 'nullable|integer|exists:nampans,id', + 'nampan_id' => 'nullable|integer', 'nama_pembeli' => 'nullable|string|max:255', - ])); - + ]); + return $this->laporanService->exportPerProduk($validated); } catch (\Exception $e) { - Log::error('Error in exprot per nampan method: ' . $e->getMessage()); + Log::error('Error in export per produk: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500); } } diff --git a/app/Http/Requests/DetailLaporanRequest.php b/app/Http/Requests/DetailLaporanRequest.php index 5a2f308..7cfc841 100644 --- a/app/Http/Requests/DetailLaporanRequest.php +++ b/app/Http/Requests/DetailLaporanRequest.php @@ -20,7 +20,8 @@ class DetailLaporanRequest extends FormRequest public function rules(): array { return [ - 'tanggal' => 'required|date_format:Y-m-d|before_or_equal:today', + 'start_date' => 'required|date_format:Y-m-d', + 'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date', 'sales_id' => 'nullable|integer|exists:sales,id', 'nampan_id' => 'nullable|integer', 'produk_id' => 'nullable|integer|exists:produks,id', @@ -36,9 +37,7 @@ class DetailLaporanRequest extends FormRequest public function messages(): array { return [ - 'tanggal.required' => 'Tanggal harus diisi', - 'tanggal.date_format' => 'Format tanggal harus Y-m-d', - 'tanggal.before_or_equal' => 'Tanggal tidak boleh lebih dari hari ini', + 'end_date.after_or_equal' => 'Tanggal akhir harus sama atau setelah tanggal mulai.', 'sales_id.exists' => 'Sales tidak ditemukan', 'produk_id.exists' => 'Produk tidak ditemukan', 'nama_pembeli.max' => 'Nama pembeli maksimal 255 karakter', diff --git a/app/Services/LaporanService.php b/app/Services/LaporanService.php index c3b8b40..5d05a18 100644 --- a/app/Services/LaporanService.php +++ b/app/Services/LaporanService.php @@ -62,12 +62,19 @@ class LaporanService */ public function getDetailPerProduk(array $params) { - $tanggal = Carbon::parse($params['tanggal']); + $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: Calculate overall totals for all filtered items --- - $totalsQuery = $this->buildBaseItemQuery($tanggal); + // --- Step 1: Totals --- + $totalsQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); // FIXED: Call benar $this->applyFilters($totalsQuery, $params); $totalsResult = $totalsQuery->select( @@ -76,14 +83,14 @@ class LaporanService DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan') )->first(); - $rekapHarian = [ + $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: Build the filtered sales data subquery --- - $salesSubQuery = $this->buildBaseItemQuery($tanggal) + // --- Step 2: Subquery --- + $salesSubQuery = $this->buildBaseItemQueryForRange($startDate, $endDate) ->select( 'produks.id as id_produk', DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), @@ -94,7 +101,7 @@ class LaporanService $this->applyFilters($salesSubQuery, $params); - // --- Step 3: Fetch paginated products and LEFT JOIN the sales data subquery --- + // --- Step 3: Paginated products --- $semuaProdukPaginated = Produk::select( 'produks.id', 'produks.nama as nama_produk', @@ -108,7 +115,7 @@ class LaporanService ->orderBy('produks.nama') ->paginate($perPage, ['*'], 'page', $page); - // --- Step 4: Map results for final presentation --- + // --- Step 4: Map & filter --- $detailItem = $semuaProdukPaginated->map(function ($item) { return [ 'nama_produk' => $item->nama_produk, @@ -122,30 +129,44 @@ class LaporanService $paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator( $detailItem->forPage($page, $perPage), - $detailItem->count(), + $detailItem->count(), // FIXED: Total dari filtered $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); - // --- Step 5: Assemble final response --- - $filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params); + + // --- Step 5: Response --- + $filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params); return [ 'filter' => $filterInfo, - 'rekap_harian' => $rekapHarian, - // 'produk' => $detailItem, + 'rekap_interval' => $rekapInterval, 'produk' => $paginatedFiltered->getCollection(), - 'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated), + '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) { - $tanggal = Carbon::parse($params['tanggal']); + $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->buildBaseItemQuery($tanggal); + $nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); // FIXED: Range $this->applyNampanFilters($nampanTerjualQuery, $params); $nampanTerjual = $nampanTerjualQuery @@ -159,16 +180,22 @@ class LaporanService ->get() ->keyBy('nama_nampan'); - $totals = $this->helper->calculateTotals($nampanTerjual); + // 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); - $filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params); $detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual) - ->filter(function ($item) { // TAMBAH: Filter out kosong - return $item['jumlah_item_terjual'] !== $this->helper::DEFAULT_DISPLAY && $item['jumlah_item_terjual'] > 0; + ->filter(function ($item) { + return $item['jumlah_item_terjual'] > 0; // FIXED: Int compare, no DEFAULT_DISPLAY check }); - // Rebuild paginator serupa seperti di atas $paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator( $detailItem->forPage($page, $perPage), $detailItem->count(), @@ -177,9 +204,11 @@ class LaporanService ['path' => request()->url(), 'query' => request()->query()] ); + $filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params); // FIXED: Range helper + return [ 'filter' => $filterInfo, - 'rekap_harian' => $totals, + 'rekap_interval' => $totals, // FIXED: Rename 'nampan' => $paginatedFiltered->getCollection(), 'pagination' => $this->helper->buildPaginationInfo($paginatedFiltered), ]; @@ -217,22 +246,22 @@ class LaporanService public function exportPerProduk(array $params) { - $tanggal = $params['tanggal']; $format = $params['format']; - $allParams = $params; unset($allParams['page'], $allParams['per_page']); $data = $this->getDetailPerProdukForExport($allParams); - $fileName = "laporan_per_produk_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}"; + $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', 'potrait'); + $pdf->setPaper('a4', 'portrait'); // FIXED: Typo 'potrait' return $pdf->download($fileName); } @@ -241,22 +270,22 @@ class LaporanService public function exportPerNampan(array $params) { - $tanggal = $params['tanggal']; $format = $params['format']; - $allParams = $params; unset($allParams['page'], $allParams['per_page']); $data = $this->getDetailPerNampanForExport($allParams); - $fileName = "laporan_per_nampan_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}"; + $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', 'potrait'); + $pdf->setPaper('a4', 'portrait'); return $pdf->download($fileName); } @@ -265,9 +294,10 @@ class LaporanService private function getDetailPerProdukForExport(array $params) { - $tanggal = Carbon::parse($params['tanggal']); - - $produkTerjualQuery = $this->buildBaseItemQuery($tanggal); + $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 @@ -282,7 +312,15 @@ class LaporanService ->get() ->keyBy('id_produk'); - $totals = $this->helper->calculateTotals($produkTerjual); + // 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(); @@ -296,31 +334,24 @@ class LaporanService 'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan), ]; } + return null; // Akan difilter + })->filter(); // FIXED: Filter null/kosong - return [ - 'nama_produk' => $item->nama, - 'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY, - 'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY, - 'pendapatan' => LaporanHelper::DEFAULT_DISPLAY, - ]; - })->filter(function ($item) { - return $item['jumlah_item_terjual'] > 0; - }); - - $filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params); + $filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params); return [ 'filter' => $filterInfo, - 'rekap_harian' => $totals, + 'rekap_interval' => $totals, // FIXED: Rename 'produk' => $detailItem->values(), ]; } private function getDetailPerNampanForExport(array $params) { - $tanggal = Carbon::parse($params['tanggal']); + $startDate = Carbon::parse($params['start_date'])->startOfDay(); + $endDate = Carbon::parse($params['end_date'])->endOfDay(); - $nampanTerjualQuery = $this->buildBaseItemQuery($tanggal); + $nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); $this->applyNampanFilters($nampanTerjualQuery, $params); $nampanTerjual = $nampanTerjualQuery @@ -334,9 +365,18 @@ class LaporanService ->get() ->keyBy('posisi_asal'); - $totals = $this->helper->calculateTotals($nampanTerjual); + // 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') @@ -353,22 +393,14 @@ class LaporanService 'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan), ]; } + return null; // Filter out + })->filter(); - return [ - 'nama_nampan' => $posisi, - 'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY, - 'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY, - 'pendapatan' => LaporanHelper::DEFAULT_DISPLAY, - ]; - })->filter(function ($item) { - return $item['jumlah_item_terjual'] > 0; - }); - - $filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params); + $filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params); // FIXED: Range return [ 'filter' => $filterInfo, - 'rekap_harian' => $totals, + 'rekap_interval' => $totals, 'nampan' => $detailItem->values(), ]; } @@ -391,15 +423,6 @@ class LaporanService return $this->transaksiRepo->processLaporanBulanan($allSalesNames, $page, $limitPagination); } - private function buildBaseItemQuery(Carbon $carbonDate) - { - // UBAH: Menghapus join ke tabel 'items' dan join 'produks' langsung dari 'item_transaksis' - return ItemTransaksi::query() - ->join('produks', 'item_transaksis.id_produk', '=', 'produks.id') - ->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id') - ->whereDate('transaksis.created_at', $carbonDate); - } - private function applyFilters($query, array $params): void { if (!empty($params['sales_id'])) { @@ -411,10 +434,10 @@ class LaporanService $nampanId = (int) $params['nampan_id']; if ($nampanId === -1) { $query->where('item_transaksis.posisi_asal', 'Brankas'); - } else { + } 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); + ->where('nampans.id', $nampanId); }); } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 3e78d69..8d7258c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -33,8 +33,8 @@ class DatabaseSeeder extends Seeder User::factory(2)->create(); Sales::factory(5)->create(); - for ($i=0; $i <= 30; $i++) { - if ($i != 13) { + for ($i=0; $i < 30; $i++) { + if ($i != 12) { Nampan::factory()->create([ 'nama' => 'A' . ($i + 1) ]); diff --git a/resources/js/components/DatePicker.vue b/resources/js/components/DatePicker.vue new file mode 100644 index 0000000..5b0d7bd --- /dev/null +++ b/resources/js/components/DatePicker.vue @@ -0,0 +1,206 @@ + + + diff --git a/resources/js/components/DetailPerNampan.vue b/resources/js/components/DetailPerNampan.vue index 21865d4..d56ce61 100644 --- a/resources/js/components/DetailPerNampan.vue +++ b/resources/js/components/DetailPerNampan.vue @@ -3,11 +3,15 @@
-
+
- - +
@@ -26,26 +30,27 @@
-
-
-
Total Item
-
{{ data.rekap_harian.total_item_terjual }}
-
-
-
Total Berat
-
{{ data.rekap_harian.total_berat_terjual }}
-
-
-
Total Pendapatan
-
{{ data.rekap_harian.total_pendapatan }}
-
-
-
+
Memuat data...
+
+
+
Total Item
+
{{ data.rekap_interval.total_item_terjual }}
+
+
+
Total Berat
+
{{ data.rekap_interval.total_berat_terjual }}
+
+
+
Total Pendapatan
+
{{ data.rekap_interval.total_pendapatan }}
+
+
+
@@ -151,19 +156,15 @@
- \ No newline at end of file diff --git a/resources/js/components/DetailPerProduk.vue b/resources/js/components/DetailPerProduk.vue index bca867e..89921c3 100644 --- a/resources/js/components/DetailPerProduk.vue +++ b/resources/js/components/DetailPerProduk.vue @@ -2,11 +2,16 @@

-
-
- - + +
+
+
@@ -22,29 +27,32 @@
+
-
-
-
Total Item
-
{{ data.rekap_harian.total_item_terjual }}
-
-
-
Total Berat
-
{{ data.rekap_harian.total_berat_terjual }}
-
-
-
Total Pendapatan
-
{{ data.rekap_harian.total_pendapatan }}
-
-
- -
+ +
Memuat data...
+
+
+
Total Item
+
{{ data.rekap_interval.total_item_terjual }}
+
+
+
Total Berat
+
{{ data.rekap_interval.total_berat_terjual }}
+
+
+
Total Pendapatan
+
{{ data.rekap_interval.total_pendapatan }}
+
+
+
+
+
@@ -130,6 +139,7 @@
+