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 @@
+
+