From ebb17c2a4396b2587b7b4d0ca0dbb196823cafc9 Mon Sep 17 00:00:00 2001 From: Baghaztra Date: Mon, 8 Sep 2025 14:29:17 +0700 Subject: [PATCH] [update] halaman laporan --- app/Http/Controllers/LaporanController.php | 445 ++++++++++++++------ resources/js/components/DetailLaporan.vue | 100 ----- resources/js/components/DetailPerNampan.vue | 420 ++++++++++++++++++ resources/js/components/DetailPerProduk.vue | 420 ++++++++++++++++++ resources/js/components/TrayList.vue | 12 +- resources/js/pages/Home.vue | 32 +- resources/js/pages/Laporan.vue | 46 +- resources/js/router/index.js | 6 + routes/web.php | 7 +- 9 files changed, 1226 insertions(+), 262 deletions(-) delete mode 100644 resources/js/components/DetailLaporan.vue create mode 100644 resources/js/components/DetailPerNampan.vue create mode 100644 resources/js/components/DetailPerProduk.vue diff --git a/app/Http/Controllers/LaporanController.php b/app/Http/Controllers/LaporanController.php index 32199ec..ab72a92 100644 --- a/app/Http/Controllers/LaporanController.php +++ b/app/Http/Controllers/LaporanController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers; use App\Models\ItemTransaksi; use App\Models\Produk; use App\Models\Transaksi; +use App\Models\Sales; +use App\Models\Nampan; use Carbon\Carbon; use Carbon\CarbonPeriod; use Illuminate\Http\Request; @@ -14,6 +16,10 @@ use Illuminate\Support\Facades\DB; class LaporanController extends Controller { + private const CURRENCY_SYMBOL = 'Rp '; + private const WEIGHT_UNIT = ' g'; + private const DEFAULT_DISPLAY = '-'; + public function ringkasan(Request $request) { $filter = $request->query('filter', 'bulan'); @@ -28,10 +34,122 @@ class LaporanController extends Controller return $this->laporanBulanan($page, $allSalesNames); } + public function detailPerProduk(Request $request) + { + $request->validate([ + 'tanggal' => 'required|date_format:Y-m-d', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $tanggal = $request->query('tanggal'); + $salesId = $request->query('sales_id'); + $nampanId = $request->query('nampan_id'); + $namaPembeli = $request->query('nama_pembeli'); + $page = $request->query('page', 1); + $perPage = $request->query('per_page', 15); + + $carbonDate = Carbon::parse($tanggal); + + $produkTerjualQuery = $this->buildBaseItemQuery($carbonDate); + $this->applyFilters($produkTerjualQuery, $salesId, $nampanId, $namaPembeli); + + $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('SUM(produks.berat) as berat_terjual'), + DB::raw('SUM(item_transaksis.harga_deal) as pendapatan') + ) + ->groupBy('produks.id', 'produks.nama') + ->get() + ->keyBy('id_produk'); + + $totals = $this->calculateTotals($produkTerjual); + $semuaProdukPaginated = Produk::select('id', 'nama')->orderBy('nama')->paginate($perPage, ['*'], 'page', $page); + $detailItem = $this->mapProductsWithSalesData($semuaProdukPaginated, $produkTerjual); + $filterInfo = $this->buildFilterInfo($carbonDate, $salesId, $nampanId, $namaPembeli); + + return response()->json([ + 'filter' => $filterInfo, + 'rekap_harian' => $totals, + 'produk' => $detailItem->values(), + 'pagination' => $this->buildPaginationInfo($semuaProdukPaginated), + ]); + } + + public function detailPerNampan(Request $request) + { + $request->validate([ + 'tanggal' => 'required|date_format:Y-m-d', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $tanggal = $request->query('tanggal'); + $salesId = $request->query('sales_id'); + $produkId = $request->query('produk_id'); + $namaPembeli = $request->query('nama_pembeli'); + $page = $request->query('page', 1); + $perPage = $request->query('per_page', 15); + + $carbonDate = Carbon::parse($tanggal); + + // Query untuk mendapatkan data penjualan per nampan + $nampanTerjualQuery = $this->buildBaseItemQuery($carbonDate); + $this->applyNampanFilters($nampanTerjualQuery, $salesId, $produkId, $namaPembeli); + + // Menggunakan COALESCE untuk menggabungkan nampan dan brankas + $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('SUM(produks.berat) as berat_terjual'), + DB::raw('SUM(item_transaksis.harga_deal) as pendapatan') + ) + ->groupBy('id_nampan', 'nama_nampan') + ->get() + ->keyBy('id_nampan'); + + $totals = $this->calculateTotals($nampanTerjual); + + // Mendapatkan semua nampan + entry untuk brankas + $semuaNampan = Nampan::select('id', 'nama')->orderBy('nama')->get(); + + // Tambahkan entry brankas (id = 0) + $brankasEntry = (object) ['id' => 0, 'nama' => 'Brankas']; + $semuaNampanCollection = $semuaNampan->prepend($brankasEntry); + + // Pagination manual + $currentPage = $page; + $offset = ($currentPage - 1) * $perPage; + $itemsForCurrentPage = $semuaNampanCollection->slice($offset, $perPage); + + $semuaNampanPaginated = new LengthAwarePaginator( + $itemsForCurrentPage, + $semuaNampanCollection->count(), + $perPage, + $currentPage, + ['path' => request()->url(), 'query' => request()->query()] + ); + + $detailItem = $this->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual); + $filterInfo = $this->buildNampanFilterInfo($carbonDate, $salesId, $produkId, $namaPembeli); + + return response()->json([ + 'filter' => $filterInfo, + 'rekap_harian' => $totals, + 'nampan' => $detailItem->values(), + 'pagination' => $this->buildPaginationInfo($semuaNampanPaginated), + ]); + } + private function laporanHarian(int $page, Collection $allSalesNames) { $perPage = 7; - $endDate = Carbon::today()->subDays(($page - 1) * $perPage); $startDate = $endDate->copy()->subDays($perPage - 1); @@ -53,7 +171,6 @@ class LaporanController extends Controller if (isset($transaksisByDay[$dateString])) { $transaksisPerTanggal = $transaksisByDay[$dateString]; - $salesDataTransaksi = $transaksisPerTanggal->groupBy('nama_sales') ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); @@ -67,21 +184,22 @@ class LaporanController extends Controller $laporan[$dateString] = [ 'tanggal' => $tanggalFormatted, - 'total_item_terjual' => $totalItem > 0 ? $totalItem : '-', - 'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-', - 'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-', + 'total_item_terjual' => $totalItem > 0 ? $totalItem : self::DEFAULT_DISPLAY, + 'total_berat' => $totalBerat > 0 ? $this->formatWeight($totalBerat) : self::DEFAULT_DISPLAY, + 'total_pendapatan' => $totalPendapatan > 0 ? $this->formatCurrency($totalPendapatan) : self::DEFAULT_DISPLAY, 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), ]; } else { $laporan[$dateString] = [ 'tanggal' => $tanggalFormatted, - 'total_item_terjual' => '-', - 'total_berat' => '-', - 'total_pendapatan' => '-', + 'total_item_terjual' => self::DEFAULT_DISPLAY, + 'total_berat' => self::DEFAULT_DISPLAY, + 'total_pendapatan' => self::DEFAULT_DISPLAY, 'sales' => [], ]; } } + $totalHariUntukPaginasi = 365; $paginatedData = new LengthAwarePaginator( array_reverse(array_values($laporan)), @@ -104,30 +222,28 @@ class LaporanController extends Controller $laporan = $transaksis->groupBy(function ($transaksi) { return Carbon::parse($transaksi->created_at)->format('F Y'); - }) - ->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { + })->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { + $salesDataTransaksi = $transaksisPerTanggal + ->groupBy('nama_sales') + ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); - $salesDataTransaksi = $transaksisPerTanggal - ->groupBy('nama_sales') - ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); - - $fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) { - return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales); - }); - - $totalItem = $fullSalesData->sum('item_terjual'); - $totalBerat = $fullSalesData->sum('berat_terjual_raw'); - $totalPendapatan = $fullSalesData->sum('pendapatan_raw'); - - return [ - 'tanggal' => $tanggal, - 'total_item_terjual' => $totalItem > 0 ? $totalItem : '-', - 'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-', - 'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-', - 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), - ]; + $fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) { + return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales); }); + $totalItem = $fullSalesData->sum('item_terjual'); + $totalBerat = $fullSalesData->sum('berat_terjual_raw'); + $totalPendapatan = $fullSalesData->sum('pendapatan_raw'); + + return [ + 'tanggal' => $tanggal, + 'total_item_terjual' => $totalItem > 0 ? $totalItem : self::DEFAULT_DISPLAY, + 'total_berat' => $totalBerat > 0 ? $this->formatWeight($totalBerat) : self::DEFAULT_DISPLAY, + 'total_pendapatan' => $totalPendapatan > 0 ? $this->formatCurrency($totalPendapatan) : self::DEFAULT_DISPLAY, + 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), + ]; + }); + $paginatedData = new LengthAwarePaginator( $laporan->forPage($page, $perPage)->values(), $laporan->count(), @@ -139,12 +255,175 @@ class LaporanController extends Controller return response()->json($paginatedData); } + private function buildBaseItemQuery(Carbon $carbonDate) + { + return ItemTransaksi::query() + ->join('items', 'item_transaksis.id_item', '=', 'items.id') + ->join('produks', 'items.id_produk', '=', 'produks.id') + ->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id') + ->whereDate('transaksis.created_at', $carbonDate); + } + + private function applyFilters($query, $salesId, $nampanId, $namaPembeli) + { + if ($salesId) { + $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') + ->where('sales.id', $salesId); + } + + if ($nampanId) { + if ($nampanId == 0) { + // Filter untuk brankas (id_nampan = null) + $query->whereNull('items.id_nampan'); + } else { + // Filter untuk nampan tertentu + $query->where('items.id_nampan', $nampanId); + } + } + + if ($namaPembeli) { + $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); + } + } + + private function applyNampanFilters($query, $salesId, $produkId, $namaPembeli) + { + if ($salesId) { + $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') + ->where('sales.id', $salesId); + } + + if ($produkId) { + $query->where('produks.id', $produkId); + } + + if ($namaPembeli) { + $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); + } + } + + private function calculateTotals(Collection $data): array + { + $totalPendapatan = $data->sum('pendapatan'); + $totalItemTerjual = $data->sum('jumlah_item_terjual'); + $totalBeratTerjual = $data->sum('berat_terjual'); + + return [ + 'total_item_terjual' => $totalItemTerjual, + 'total_berat_terjual' => $this->formatWeight($totalBeratTerjual), + 'total_pendapatan' => $this->formatCurrency($totalPendapatan), + ]; + } + + private function mapProductsWithSalesData($paginatedData, Collection $salesData): Collection + { + return $paginatedData->getCollection()->map(function ($item) use ($salesData) { + if ($salesData->has($item->id)) { + $dataTerjual = $salesData->get($item->id); + return [ + 'nama_produk' => $item->nama, + 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, + 'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual), + 'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan), + ]; + } + + return [ + 'nama_produk' => $item->nama, + 'jumlah_item_terjual' => self::DEFAULT_DISPLAY, + 'berat_terjual' => self::DEFAULT_DISPLAY, + 'pendapatan' => self::DEFAULT_DISPLAY, + ]; + }); + } + + private function mapNampanWithSalesData($paginatedData, Collection $salesData): Collection + { + return $paginatedData->getCollection()->map(function ($item) use ($salesData) { + if ($salesData->has($item->id)) { + $dataTerjual = $salesData->get($item->id); + return [ + 'nama_nampan' => $item->nama, + 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, + 'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual), + 'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan), + ]; + } + + return [ + 'nama_nampan' => $item->nama, + 'jumlah_item_terjual' => self::DEFAULT_DISPLAY, + 'berat_terjual' => self::DEFAULT_DISPLAY, + 'pendapatan' => self::DEFAULT_DISPLAY, + ]; + }); + } + + private function buildFilterInfo(Carbon $carbonDate, $salesId, $nampanId, $namaPembeli): array + { + $filterInfo = [ + 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), + 'nama_sales' => null, + 'nampan' => null, + 'nama_pembeli' => $namaPembeli, + ]; + + if ($salesId) { + $sales = Sales::find($salesId); + $filterInfo['nama_sales'] = $sales?->nama; + } + + if ($nampanId) { + if ($nampanId == 0) { + $filterInfo['nampan'] = 'Brankas'; + } else { + $nampan = Nampan::find($nampanId); + $filterInfo['nampan'] = $nampan?->nama; + } + } + + return $filterInfo; + } + + private function buildNampanFilterInfo(Carbon $carbonDate, $salesId, $produkId, $namaPembeli): array + { + $filterInfo = [ + 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), + 'nama_sales' => null, + 'produk' => null, + 'nama_pembeli' => $namaPembeli, + ]; + + if ($salesId) { + $sales = Sales::find($salesId); + $filterInfo['nama_sales'] = $sales?->nama; + } + + if ($produkId) { + $produk = Produk::find($produkId); + $filterInfo['produk'] = $produk?->nama; + } + + return $filterInfo; + } + + private function buildPaginationInfo($paginatedData): array + { + return [ + 'current_page' => $paginatedData->currentPage(), + 'last_page' => $paginatedData->lastPage(), + 'per_page' => $paginatedData->perPage(), + 'total' => $paginatedData->total(), + 'from' => $paginatedData->firstItem(), + 'to' => $paginatedData->lastItem(), + ]; + } + private function hitungDataSales(Collection $transaksisPerSales): array { $itemTerjual = $transaksisPerSales->sum(fn($t) => $t->itemTransaksi->count()); $beratTerjual = $transaksisPerSales->sum( - fn($t) => - $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0) + fn($t) => $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0) ); $pendapatan = $transaksisPerSales->sum('total_harga'); @@ -169,108 +448,22 @@ class LaporanController extends Controller private function formatSalesDataValues(Collection $salesData): Collection { return $salesData->map(function ($sale) { - $sale['item_terjual'] = $sale['item_terjual'] > 0 ? $sale['item_terjual'] : '-'; - $sale['berat_terjual'] = $sale['berat_terjual_raw'] > 0 ? number_format($sale['berat_terjual_raw'], 2, ',', '.') . 'g' : '-'; - $sale['pendapatan'] = $sale['pendapatan_raw'] > 0 ? 'Rp' . number_format($sale['pendapatan_raw'], 2, ',', '.') : '-'; + $sale['item_terjual'] = $sale['item_terjual'] > 0 ? $sale['item_terjual'] : self::DEFAULT_DISPLAY; + $sale['berat_terjual'] = $sale['berat_terjual_raw'] > 0 ? $this->formatWeight($sale['berat_terjual_raw']) : self::DEFAULT_DISPLAY; + $sale['pendapatan'] = $sale['pendapatan_raw'] > 0 ? $this->formatCurrency($sale['pendapatan_raw']) : self::DEFAULT_DISPLAY; unset($sale['berat_terjual_raw'], $sale['pendapatan_raw']); return $sale; }); } -public function detail(Request $request) + private function formatCurrency(float $amount): string { - // 1. VALIDASI DAN PENGAMBILAN PARAMETER FILTER - $request->validate([ - 'tanggal' => 'required|date_format:Y-m-d', - ]); + return self::CURRENCY_SYMBOL . number_format($amount, 2, ',', '.'); + } - $tanggal = $request->query('tanggal'); - $namaSales = $request->query('nama_sales'); - $posisi = $request->query('posisi'); - $namaPembeli = $request->query('nama_pembeli'); // Untuk pencarian - - $carbonDate = Carbon::parse($tanggal); - - // 2. QUERY UTAMA UNTUK MENGAMBIL DATA PRODUK YANG TERJUAL BERDASARKAN FILTER - // Query ini hanya akan mengambil produk yang memiliki transaksi sesuai filter. - $produkTerjualQuery = ItemTransaksi::query() - ->join('items', 'item_transaksis.id_item', '=', 'items.id') - ->join('produks', 'items.id_produk', '=', 'produks.id') - ->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id') - // Filter Wajib: Tanggal - ->whereDate('transaksis.created_at', $carbonDate) - // Filter Opsional: Nama Sales - ->when($namaSales, function ($query, $namaSales) { - return $query->where('transaksis.nama_sales', $namaSales); - }) - // Filter Opsional: Posisi Asal Item - ->when($posisi, function ($query, $posisi) { - return $query->where('item_transaksis.posisi_asal', $posisi); - }) - // Filter Opsional: Nama Pembeli (menggunakan LIKE untuk pencarian) - ->when($namaPembeli, function ($query, $namaPembeli) { - return $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); - }) - ->select( - 'produks.id as id_produk', - 'produks.nama as nama_produk', - DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), - DB::raw('SUM(produks.berat) as berat_terjual'), - DB::raw('SUM(item_transaksis.harga_deal) as pendapatan') - ) - ->groupBy('produks.id', 'produks.nama') - ->get() - // Mengubah collection menjadi array asosiatif dengan key id_produk agar mudah dicari - ->keyBy('id_produk'); - - - // 3. MENGAMBIL SEMUA PRODUK DARI DATABASE - $semuaProduk = Produk::query()->select('id', 'nama')->get(); - - // 4. MENGGABUNGKAN DATA SEMUA PRODUK DENGAN PRODUK YANG TERJUAL - $detailItem = $semuaProduk->map(function ($produk) use ($produkTerjualQuery) { - // Cek apakah produk ini ada di dalam daftar produk yang terjual - if ($produkTerjualQuery->has($produk->id)) { - $dataTerjual = $produkTerjualQuery->get($produk->id); - return [ - 'nama_produk' => $produk->nama, - 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, - 'berat_terjual' => (float) $dataTerjual->berat_terjual, - 'pendapatan' => (float) $dataTerjual->pendapatan, - ]; - } else { - // Jika produk tidak terjual, berikan nilai default "-" - return [ - 'nama_produk' => $produk->nama, - 'jumlah_item_terjual' => '-', - 'berat_terjual' => '-', - 'pendapatan' => '-', - ]; - } - }); - - // 5. MENGHITUNG TOTAL REKAP HARIAN DARI DATA YANG SUDAH DIFILTER - $totalPendapatan = $produkTerjualQuery->sum('pendapatan'); - $totalItemTerjual = $produkTerjualQuery->sum('jumlah_item_terjual'); - $totalBeratTerjual = $produkTerjualQuery->sum('berat_terjual'); - - // 6. MENYUSUN STRUKTUR RESPONSE FINAL - $response = [ - 'filter' => [ - 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), - 'nama_sales' => $namaSales, - 'posisi' => $posisi, - 'nama_pembeli' => $namaPembeli, - ], - 'rekap_harian' => [ - 'total_item_terjual' => $totalItemTerjual, - 'total_berat_terjual' => $totalBeratTerjual, - 'total_pendapatan' => $totalPendapatan, - ], - 'produk' => $detailItem, - ]; - - return response()->json($response); + private function formatWeight(float $weight): string + { + return number_format($weight, 2, ',', '.') . self::WEIGHT_UNIT; } } diff --git a/resources/js/components/DetailLaporan.vue b/resources/js/components/DetailLaporan.vue deleted file mode 100644 index a5c113a..0000000 --- a/resources/js/components/DetailLaporan.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/js/components/DetailPerNampan.vue b/resources/js/components/DetailPerNampan.vue new file mode 100644 index 0000000..1687899 --- /dev/null +++ b/resources/js/components/DetailPerNampan.vue @@ -0,0 +1,420 @@ + + + \ No newline at end of file diff --git a/resources/js/components/DetailPerProduk.vue b/resources/js/components/DetailPerProduk.vue new file mode 100644 index 0000000..0de73b3 --- /dev/null +++ b/resources/js/components/DetailPerProduk.vue @@ -0,0 +1,420 @@ + + + \ No newline at end of file diff --git a/resources/js/components/TrayList.vue b/resources/js/components/TrayList.vue index 9d0d9eb..addc53e 100644 --- a/resources/js/components/TrayList.vue +++ b/resources/js/components/TrayList.vue @@ -164,8 +164,16 @@ const saveMove = async () => { const refreshData = async () => { try { const [nampanRes, itemRes] = await Promise.all([ - axios.get("/api/nampan"), - axios.get("/api/item"), + axios.get("/api/nampan", { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }), + axios.get("/api/item", { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }), ]); const nampans = nampanRes.data; const items = itemRes.data; diff --git a/resources/js/pages/Home.vue b/resources/js/pages/Home.vue index b1d8a59..0100f54 100644 --- a/resources/js/pages/Home.vue +++ b/resources/js/pages/Home.vue @@ -1,12 +1,11 @@