diff --git a/app/Helpers/LaporanHelper.php b/app/Helpers/LaporanHelper.php index 700c9b1..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, @@ -57,15 +58,15 @@ class LaporanHelper $dataTerjual = $salesData->get($item->id); return [ 'nama_produk' => $item->nama, - 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, + 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, // Selalu int 'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual), 'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan), ]; } - return [ + return [ // Untuk kosong, return dengan 0 (akan difilter nanti) 'nama_produk' => $item->nama, - 'jumlah_item_terjual' => self::DEFAULT_DISPLAY, + 'jumlah_item_terjual' => 0, 'berat_terjual' => self::DEFAULT_DISPLAY, 'pendapatan' => self::DEFAULT_DISPLAY, ]; @@ -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/Controllers/ProdukController.php b/app/Http/Controllers/ProdukController.php index 3385cf6..140608a 100644 --- a/app/Http/Controllers/ProdukController.php +++ b/app/Http/Controllers/ProdukController.php @@ -213,7 +213,7 @@ class ProdukController extends Controller $foto->delete(); } - // Hapus produk (soft delete) + $produk->items()->delete(); $produk->delete(); DB::commit(); 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/Models/Item.php b/app/Models/Item.php index 7fcec9e..1fdd3b6 100644 --- a/app/Models/Item.php +++ b/app/Models/Item.php @@ -24,7 +24,7 @@ class Item extends Model parent::boot(); static::creating(function ($item) { - $prefix = 'ITM'; + $prefix = 'TMJC'; $date = now()->format('Ymd'); // Cari item terakhir yg dibuat hari ini diff --git a/app/Services/LaporanService.php b/app/Services/LaporanService.php index 1e280db..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, @@ -116,26 +123,50 @@ class LaporanService '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; }); - // --- Step 5: Assemble final response --- - $filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params); + $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_harian' => $rekapHarian, - 'produk' => $detailItem, - 'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated), + '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) { - $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 @@ -149,16 +180,37 @@ 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) { + 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_harian' => $totals, - 'nampan' => $detailItem->values(), - 'pagination' => $this->helper->buildPaginationInfo($semuaNampanPaginated), + 'rekap_interval' => $totals, // FIXED: Rename + 'nampan' => $paginatedFiltered->getCollection(), + 'pagination' => $this->helper->buildPaginationInfo($paginatedFiltered), ]; } @@ -194,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); } @@ -218,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); } @@ -242,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 @@ -259,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(); @@ -273,29 +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, - ]; - }); - - $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 @@ -309,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') @@ -328,20 +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, - ]; - }); - - $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(), ]; } @@ -364,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'])) { @@ -381,11 +431,14 @@ class LaporanService } if (isset($params['nampan_id'])) { - // UBAH: Filter berdasarkan 'item_transaksis.id_nampan' - if ($params['nampan_id'] == 0) { - $query->whereNull('item_transaksis.id_nampan'); - } else { - $query->where('item_transaksis.id_nampan', $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); + }); } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 3e7500e..8d7258c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -33,11 +33,10 @@ class DatabaseSeeder extends Seeder User::factory(2)->create(); Sales::factory(5)->create(); - $kodeNampan = ['A', 'B']; - foreach ($kodeNampan as $kode) { - for ($i=0; $i < 4; $i++) { + for ($i=0; $i < 30; $i++) { + if ($i != 12) { Nampan::factory()->create([ - 'nama' => $kode . ($i + 1) // A1, A2, ... B4 + 'nama' => 'A' . ($i + 1) ]); } } diff --git a/public/logo.ico b/public/logo.ico new file mode 100644 index 0000000..e8cb3af Binary files /dev/null and b/public/logo.ico differ diff --git a/resources/js/components/BrankasList.vue b/resources/js/components/BrankasList.vue index ffda540..904ea91 100644 --- a/resources/js/components/BrankasList.vue +++ b/resources/js/components/BrankasList.vue @@ -340,40 +340,50 @@ const printQR = () => {