From fd328b6e3594a128291238213cb687632093b332 Mon Sep 17 00:00:00 2001 From: Baghaztra Date: Wed, 3 Sep 2025 13:53:06 +0700 Subject: [PATCH] [Feat] Ringkasan laporan --- app/Http/Controllers/LaporanController.php | 176 +++++++++++++ database/factories/TransaksiFactory.php | 4 +- resources/js/components/InputField.vue | 2 +- resources/js/components/KasirForm.vue | 2 +- resources/js/components/RingkasanLaporanA.vue | 241 ++++++++++++++++++ resources/js/components/RingkasanLaporanB.vue | 239 +++++++++++++++++ resources/js/components/searchbar.vue | 2 +- resources/js/pages/Kasir.vue | 2 +- resources/js/pages/Laporan.vue | 15 ++ resources/js/router/index.js | 6 + routes/web.php | 4 + 11 files changed, 688 insertions(+), 5 deletions(-) create mode 100644 app/Http/Controllers/LaporanController.php create mode 100644 resources/js/components/RingkasanLaporanA.vue create mode 100644 resources/js/components/RingkasanLaporanB.vue create mode 100644 resources/js/pages/Laporan.vue diff --git a/app/Http/Controllers/LaporanController.php b/app/Http/Controllers/LaporanController.php new file mode 100644 index 0000000..e7e09b7 --- /dev/null +++ b/app/Http/Controllers/LaporanController.php @@ -0,0 +1,176 @@ +query('filter', 'bulan'); + $page = $request->query('page', 1); + + $allSalesNames = Transaksi::select('nama_sales')->distinct()->pluck('nama_sales'); + + if ($filter === 'hari') { + return $this->laporanHarian($page, $allSalesNames); + } + + return $this->laporanBulanan($page, $allSalesNames); + } + + private function laporanHarian(int $page, Collection $allSalesNames) + { + $perPage = 7; + + $endDate = Carbon::today()->subDays(($page - 1) * $perPage); + $startDate = $endDate->copy()->subDays($perPage - 1); + + $transaksis = Transaksi::with('itemTransaksi.item.produk') + ->whereBetween('created_at', [$startDate->startOfDay(), $endDate->endOfDay()]) + ->orderBy('created_at', 'desc') + ->get(); + + $transaksisByDay = $transaksis->groupBy(function ($transaksi) { + return Carbon::parse($transaksi->created_at)->format('Y-m-d'); + }); + + $period = CarbonPeriod::create($startDate, $endDate); + $laporan = []; + + foreach ($period as $date) { + $dateString = $date->format('Y-m-d'); + $tanggalFormatted = $date->isoFormat('dddd, D MMMM Y'); + + if (isset($transaksisByDay[$dateString])) { + $transaksisPerTanggal = $transaksisByDay[$dateString]; + + $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'); + + $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, ',', '.') : '-', + 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), + ]; + } else { + $laporan[$dateString] = [ + 'tanggal' => $tanggalFormatted, + 'total_item_terjual' => '-', + 'total_berat' => '-', + 'total_pendapatan' => '-', + 'sales' => [], + ]; + } + } + $totalHariUntukPaginasi = 365; + $paginatedData = new LengthAwarePaginator( + array_reverse(array_values($laporan)), + $totalHariUntukPaginasi, + $perPage, + $page, + ['path' => request()->url(), 'query' => request()->query()] + ); + + return response()->json($paginatedData); + } + + private function laporanBulanan(int $page, Collection $allSalesNames) + { + $perPage = 12; + + $transaksis = Transaksi::with('itemTransaksi.item.produk') + ->orderBy('created_at', 'desc') + ->get(); + + $laporan = $transaksis->groupBy(function ($transaksi) { + return Carbon::parse($transaksi->created_at)->format('F Y'); + }) + ->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { + + $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(), + ]; + }); + + $paginatedData = new LengthAwarePaginator( + $laporan->forPage($page, $perPage)->values(), + $laporan->count(), + $perPage, + $page, + ['path' => request()->url(), 'query' => request()->query()] + ); + + return response()->json($paginatedData); + } + + 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) + ); + $pendapatan = $transaksisPerSales->sum('total_harga'); + + return [ + 'nama' => $transaksisPerSales->first()->nama_sales, + 'item_terjual' => $itemTerjual, + 'berat_terjual_raw' => $beratTerjual, + 'pendapatan_raw' => $pendapatan, + ]; + } + + private function defaultSalesData(string $namaSales): array + { + return [ + 'nama' => $namaSales, + 'item_terjual' => 0, + 'berat_terjual_raw' => 0, + 'pendapatan_raw' => 0, + ]; + } + + 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, ',', '.') : '-'; + + unset($sale['berat_terjual_raw'], $sale['pendapatan_raw']); + return $sale; + }); + } +} \ No newline at end of file diff --git a/database/factories/TransaksiFactory.php b/database/factories/TransaksiFactory.php index 81b6cfb..999c8bb 100644 --- a/database/factories/TransaksiFactory.php +++ b/database/factories/TransaksiFactory.php @@ -22,6 +22,7 @@ class TransaksiFactory extends Factory $sales = Sales::inRandomOrder()->first(); $kasir = User::inRandomOrder()->first(); + $date = $this->faker->dateTimeBetween('-3 months'); return [ 'id_kasir' => $kasir?->id, 'id_sales' => $sales?->id, @@ -31,7 +32,8 @@ class TransaksiFactory extends Factory 'alamat' => $this->faker->address(), 'ongkos_bikin' => $this->faker->randomFloat(2, 0, 1000000), 'total_harga' => $this->faker->randomFloat(2, 100000, 5000000), - 'created_at' => now(), + 'created_at' => $date, + 'updated_at' => $date, ]; } } diff --git a/resources/js/components/InputField.vue b/resources/js/components/InputField.vue index b82db89..053e2b5 100644 --- a/resources/js/components/InputField.vue +++ b/resources/js/components/InputField.vue @@ -4,7 +4,7 @@ :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :placeholder="placeholder" - class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2" + class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2" /> diff --git a/resources/js/components/KasirForm.vue b/resources/js/components/KasirForm.vue index 582e4d2..d39f2a7 100644 --- a/resources/js/components/KasirForm.vue +++ b/resources/js/components/KasirForm.vue @@ -6,7 +6,7 @@
+ class=" bg-A focus:outline-none focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full rounded-l-md" />
diff --git a/resources/js/components/RingkasanLaporanA.vue b/resources/js/components/RingkasanLaporanA.vue new file mode 100644 index 0000000..ef06b6a --- /dev/null +++ b/resources/js/components/RingkasanLaporanA.vue @@ -0,0 +1,241 @@ + + + \ No newline at end of file diff --git a/resources/js/components/RingkasanLaporanB.vue b/resources/js/components/RingkasanLaporanB.vue new file mode 100644 index 0000000..9de8efb --- /dev/null +++ b/resources/js/components/RingkasanLaporanB.vue @@ -0,0 +1,239 @@ + + + \ No newline at end of file diff --git a/resources/js/components/searchbar.vue b/resources/js/components/searchbar.vue index 83cad50..6c66972 100644 --- a/resources/js/components/searchbar.vue +++ b/resources/js/components/searchbar.vue @@ -4,7 +4,7 @@ v-model="searchText" type="text" placeholder="Cari ..." - class="border rounded-md px-3 py-2 w-64 focus:outline-none focus:ring-2 focus:ring-blue-400" + class="border border-C bg-A rounded-md px-3 py-2 w-64 focus:outline-none focus:ring-2 focus:ring-blue-400" @input="$emit('update:search', searchText)" />
diff --git a/resources/js/pages/Kasir.vue b/resources/js/pages/Kasir.vue index cb19e0d..074f3ad 100644 --- a/resources/js/pages/Kasir.vue +++ b/resources/js/pages/Kasir.vue @@ -1,7 +1,7 @@