[Update] Filter interval laporan

This commit is contained in:
Baghaztra 2025-09-19 13:26:53 +07:00
parent 97192bb05a
commit f71fabdc90
10 changed files with 473 additions and 175 deletions

View File

@ -18,9 +18,10 @@ class LaporanHelper
public function calculateTotals(Collection $data): array public function calculateTotals(Collection $data): array
{ {
$totalPendapatan = $data->sum('pendapatan'); // Asumsi $data punya raw numeric (int/float)
$totalItemTerjual = $data->sum('jumlah_item_terjual'); $totalPendapatan = $data->sum('pendapatan'); // Raw float
$totalBeratTerjual = $data->sum('berat_terjual'); $totalItemTerjual = $data->sum('jumlah_item_terjual'); // Int
$totalBeratTerjual = $data->sum('berat_terjual'); // Float
return [ return [
'total_item_terjual' => $totalItemTerjual, '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 = [ $filterInfo = [
'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), 'periode' => "{$startDate->isoFormat('D MMMM Y')} - {$endDate->isoFormat('D MMMM Y')}",
'nama_sales' => null, 'nama_sales' => null,
'nampan' => null, 'nampan' => null, // Default null
'nama_pembeli' => $params['nama_pembeli'] ?? null, 'nama_pembeli' => $params['nama_pembeli'] ?? null,
]; ];
@ -109,21 +110,23 @@ class LaporanHelper
} }
if (isset($params['nampan_id'])) { if (isset($params['nampan_id'])) {
if ($params['nampan_id'] == 0) { if ($params['nampan_id'] === -1) {
$filterInfo['nampan'] = 'Brankas'; $filterInfo['nampan'] = 'Brankas';
} else { } elseif ($params['nampan_id'] > 0) {
$nampan = Nampan::find($params['nampan_id']); $nampan = Nampan::find($params['nampan_id']);
$filterInfo['nampan'] = $nampan?->nama; $filterInfo['nampan'] = $nampan?->nama;
} else { // 0: Semua
$filterInfo['nampan'] = 'Semua Nampan';
} }
} }
return $filterInfo; return $filterInfo;
} }
public function buildNampanFilterInfo(Carbon $carbonDate, array $params): array public function buildNampanFilterInfo(Carbon $startDate, Carbon $endDate, array $params): array
{ {
$filterInfo = [ $filterInfo = [
'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), 'periode' => "{$startDate->isoFormat('D MMMM Y')} - {$endDate->isoFormat('D MMMM Y')}", // FIXED: Range
'nama_sales' => null, 'nama_sales' => null,
'produk' => null, 'produk' => null,
'nama_pembeli' => $params['nama_pembeli'] ?? null, 'nama_pembeli' => $params['nama_pembeli'] ?? null,

View File

@ -88,17 +88,18 @@ class LaporanController extends Controller
public function exportDetailNampan(Request $request) public function exportDetailNampan(Request $request)
{ {
try { try {
return $this->laporanService->exportPerNampan($request->validate([ $validated = $request->validate([
'tanggal' => 'required|string', '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', 'format' => 'required|string|in:pdf,xlsx,csv',
'page' => 'required|integer|min:1', 'page' => 'required|integer|min:1',
'sales_id' => 'nullable|integer|exists:sales,id', 'sales_id' => 'nullable|integer|exists:sales,id',
'produk_id' => 'nullable|integer|exists:produks,id', 'produk_id' => 'nullable|integer|exists:produks,id',
'nama_pembeli' => 'nullable|string|max:255', 'nama_pembeli' => 'nullable|string|max:255',
])); ]);
return $this->laporanService->exportPerNampan($validated);
} catch (\Exception $e) { } 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); return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
} }
} }
@ -106,17 +107,18 @@ class LaporanController extends Controller
public function exportDetailProduk(Request $request) public function exportDetailProduk(Request $request)
{ {
try { try {
return $this->laporanService->exportPerProduk($request->validate([ $validated = $request->validate([
'tanggal' => 'required|string', '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', 'format' => 'required|string|in:pdf,xlsx,csv',
'page' => 'required|integer|min:1', 'page' => 'required|integer|min:1',
'sales_id' => 'nullable|integer|exists:sales,id', 'sales_id' => 'nullable|integer|exists:sales,id',
'nampan_id' => 'nullable|integer|exists:nampans,id', 'nampan_id' => 'nullable|integer',
'nama_pembeli' => 'nullable|string|max:255', 'nama_pembeli' => 'nullable|string|max:255',
])); ]);
return $this->laporanService->exportPerProduk($validated);
} catch (\Exception $e) { } 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); return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
} }
} }

View File

@ -20,7 +20,8 @@ class DetailLaporanRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ 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', 'sales_id' => 'nullable|integer|exists:sales,id',
'nampan_id' => 'nullable|integer', 'nampan_id' => 'nullable|integer',
'produk_id' => 'nullable|integer|exists:produks,id', 'produk_id' => 'nullable|integer|exists:produks,id',
@ -36,9 +37,7 @@ class DetailLaporanRequest extends FormRequest
public function messages(): array public function messages(): array
{ {
return [ return [
'tanggal.required' => 'Tanggal harus diisi', 'end_date.after_or_equal' => 'Tanggal akhir harus sama atau setelah tanggal mulai.',
'tanggal.date_format' => 'Format tanggal harus Y-m-d',
'tanggal.before_or_equal' => 'Tanggal tidak boleh lebih dari hari ini',
'sales_id.exists' => 'Sales tidak ditemukan', 'sales_id.exists' => 'Sales tidak ditemukan',
'produk_id.exists' => 'Produk tidak ditemukan', 'produk_id.exists' => 'Produk tidak ditemukan',
'nama_pembeli.max' => 'Nama pembeli maksimal 255 karakter', 'nama_pembeli.max' => 'Nama pembeli maksimal 255 karakter',

View File

@ -62,12 +62,19 @@ class LaporanService
*/ */
public function getDetailPerProduk(array $params) 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; $page = $params['page'] ?? 1;
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE; $perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
// --- Step 1: Calculate overall totals for all filtered items --- // --- Step 1: Totals ---
$totalsQuery = $this->buildBaseItemQuery($tanggal); $totalsQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); // FIXED: Call benar
$this->applyFilters($totalsQuery, $params); $this->applyFilters($totalsQuery, $params);
$totalsResult = $totalsQuery->select( $totalsResult = $totalsQuery->select(
@ -76,14 +83,14 @@ class LaporanService
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan') DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan')
)->first(); )->first();
$rekapHarian = [ $rekapInterval = [
'total_item_terjual' => (int) $totalsResult->total_item_terjual, 'total_item_terjual' => (int) $totalsResult->total_item_terjual,
'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual), 'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual),
'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan), 'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan),
]; ];
// --- Step 2: Build the filtered sales data subquery --- // --- Step 2: Subquery ---
$salesSubQuery = $this->buildBaseItemQuery($tanggal) $salesSubQuery = $this->buildBaseItemQueryForRange($startDate, $endDate)
->select( ->select(
'produks.id as id_produk', 'produks.id as id_produk',
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
@ -94,7 +101,7 @@ class LaporanService
$this->applyFilters($salesSubQuery, $params); $this->applyFilters($salesSubQuery, $params);
// --- Step 3: Fetch paginated products and LEFT JOIN the sales data subquery --- // --- Step 3: Paginated products ---
$semuaProdukPaginated = Produk::select( $semuaProdukPaginated = Produk::select(
'produks.id', 'produks.id',
'produks.nama as nama_produk', 'produks.nama as nama_produk',
@ -108,7 +115,7 @@ class LaporanService
->orderBy('produks.nama') ->orderBy('produks.nama')
->paginate($perPage, ['*'], 'page', $page); ->paginate($perPage, ['*'], 'page', $page);
// --- Step 4: Map results for final presentation --- // --- Step 4: Map & filter ---
$detailItem = $semuaProdukPaginated->map(function ($item) { $detailItem = $semuaProdukPaginated->map(function ($item) {
return [ return [
'nama_produk' => $item->nama_produk, 'nama_produk' => $item->nama_produk,
@ -122,30 +129,44 @@ class LaporanService
$paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator( $paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator(
$detailItem->forPage($page, $perPage), $detailItem->forPage($page, $perPage),
$detailItem->count(), $detailItem->count(), // FIXED: Total dari filtered
$perPage, $perPage,
$page, $page,
['path' => request()->url(), 'query' => request()->query()] ['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 [ return [
'filter' => $filterInfo, 'filter' => $filterInfo,
'rekap_harian' => $rekapHarian, 'rekap_interval' => $rekapInterval,
// 'produk' => $detailItem,
'produk' => $paginatedFiltered->getCollection(), '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) 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; $page = $params['page'] ?? 1;
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE; $perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
$nampanTerjualQuery = $this->buildBaseItemQuery($tanggal); $nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate); // FIXED: Range
$this->applyNampanFilters($nampanTerjualQuery, $params); $this->applyNampanFilters($nampanTerjualQuery, $params);
$nampanTerjual = $nampanTerjualQuery $nampanTerjual = $nampanTerjualQuery
@ -159,16 +180,22 @@ class LaporanService
->get() ->get()
->keyBy('nama_nampan'); ->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); $semuaNampanPaginated = $this->helper->getAllNampanWithPagination($page, $perPage);
// $detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual);
$filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params);
$detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual) $detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual)
->filter(function ($item) { // TAMBAH: Filter out kosong ->filter(function ($item) {
return $item['jumlah_item_terjual'] !== $this->helper::DEFAULT_DISPLAY && $item['jumlah_item_terjual'] > 0; return $item['jumlah_item_terjual'] > 0; // FIXED: Int compare, no DEFAULT_DISPLAY check
}); });
// Rebuild paginator serupa seperti di atas
$paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator( $paginatedFiltered = new \Illuminate\Pagination\LengthAwarePaginator(
$detailItem->forPage($page, $perPage), $detailItem->forPage($page, $perPage),
$detailItem->count(), $detailItem->count(),
@ -177,9 +204,11 @@ class LaporanService
['path' => request()->url(), 'query' => request()->query()] ['path' => request()->url(), 'query' => request()->query()]
); );
$filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params); // FIXED: Range helper
return [ return [
'filter' => $filterInfo, 'filter' => $filterInfo,
'rekap_harian' => $totals, 'rekap_interval' => $totals, // FIXED: Rename
'nampan' => $paginatedFiltered->getCollection(), 'nampan' => $paginatedFiltered->getCollection(),
'pagination' => $this->helper->buildPaginationInfo($paginatedFiltered), 'pagination' => $this->helper->buildPaginationInfo($paginatedFiltered),
]; ];
@ -217,22 +246,22 @@ class LaporanService
public function exportPerProduk(array $params) public function exportPerProduk(array $params)
{ {
$tanggal = $params['tanggal'];
$format = $params['format']; $format = $params['format'];
$allParams = $params; $allParams = $params;
unset($allParams['page'], $allParams['per_page']); unset($allParams['page'], $allParams['per_page']);
$data = $this->getDetailPerProdukForExport($allParams); $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') { if ($format === 'pdf') {
$pdf = PDF::loadView('exports.perproduk_pdf', [ $pdf = PDF::loadView('exports.perproduk_pdf', [
'data' => $data, 'data' => $data,
'title' => 'Laporan Detail Per Produk' 'title' => 'Laporan Detail Per Produk'
]); ]);
$pdf->setPaper('a4', 'potrait'); $pdf->setPaper('a4', 'portrait'); // FIXED: Typo 'potrait'
return $pdf->download($fileName); return $pdf->download($fileName);
} }
@ -241,22 +270,22 @@ class LaporanService
public function exportPerNampan(array $params) public function exportPerNampan(array $params)
{ {
$tanggal = $params['tanggal'];
$format = $params['format']; $format = $params['format'];
$allParams = $params; $allParams = $params;
unset($allParams['page'], $allParams['per_page']); unset($allParams['page'], $allParams['per_page']);
$data = $this->getDetailPerNampanForExport($allParams); $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') { if ($format === 'pdf') {
$pdf = PDF::loadView('exports.pernampan_pdf', [ $pdf = PDF::loadView('exports.pernampan_pdf', [
'data' => $data, 'data' => $data,
'title' => 'Laporan Detail Per Nampan' 'title' => 'Laporan Detail Per Nampan'
]); ]);
$pdf->setPaper('a4', 'potrait'); $pdf->setPaper('a4', 'portrait');
return $pdf->download($fileName); return $pdf->download($fileName);
} }
@ -265,9 +294,10 @@ class LaporanService
private function getDetailPerProdukForExport(array $params) private function getDetailPerProdukForExport(array $params)
{ {
$tanggal = Carbon::parse($params['tanggal']); $startDate = Carbon::parse($params['start_date'])->startOfDay();
$endDate = Carbon::parse($params['end_date'])->endOfDay();
$produkTerjualQuery = $this->buildBaseItemQuery($tanggal);
$produkTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
$this->applyFilters($produkTerjualQuery, $params); $this->applyFilters($produkTerjualQuery, $params);
$produkTerjual = $produkTerjualQuery $produkTerjual = $produkTerjualQuery
@ -282,7 +312,15 @@ class LaporanService
->get() ->get()
->keyBy('id_produk'); ->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(); $semuaProduk = Produk::select('id', 'nama')->orderBy('nama')->get();
@ -296,31 +334,24 @@ class LaporanService
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan), 'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
]; ];
} }
return null; // Akan difilter
})->filter(); // FIXED: Filter null/kosong
return [ $filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params);
'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);
return [ return [
'filter' => $filterInfo, 'filter' => $filterInfo,
'rekap_harian' => $totals, 'rekap_interval' => $totals, // FIXED: Rename
'produk' => $detailItem->values(), 'produk' => $detailItem->values(),
]; ];
} }
private function getDetailPerNampanForExport(array $params) 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); $this->applyNampanFilters($nampanTerjualQuery, $params);
$nampanTerjual = $nampanTerjualQuery $nampanTerjual = $nampanTerjualQuery
@ -334,9 +365,18 @@ class LaporanService
->get() ->get()
->keyBy('posisi_asal'); ->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') $semuaPosisi = DB::table('item_transaksis')
->whereBetween('created_at', [$startDate, $endDate]) // FIXED: Filter posisi di range
->select('posisi_asal') ->select('posisi_asal')
->distinct() ->distinct()
->pluck('posisi_asal') ->pluck('posisi_asal')
@ -353,22 +393,14 @@ class LaporanService
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan), 'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
]; ];
} }
return null; // Filter out
})->filter();
return [ $filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params); // FIXED: Range
'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);
return [ return [
'filter' => $filterInfo, 'filter' => $filterInfo,
'rekap_harian' => $totals, 'rekap_interval' => $totals,
'nampan' => $detailItem->values(), 'nampan' => $detailItem->values(),
]; ];
} }
@ -391,15 +423,6 @@ class LaporanService
return $this->transaksiRepo->processLaporanBulanan($allSalesNames, $page, $limitPagination); 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 private function applyFilters($query, array $params): void
{ {
if (!empty($params['sales_id'])) { if (!empty($params['sales_id'])) {
@ -411,10 +434,10 @@ class LaporanService
$nampanId = (int) $params['nampan_id']; $nampanId = (int) $params['nampan_id'];
if ($nampanId === -1) { if ($nampanId === -1) {
$query->where('item_transaksis.posisi_asal', 'Brankas'); $query->where('item_transaksis.posisi_asal', 'Brankas');
} else { } elseif ($nampanId > 0) { // FIXED: >0 join, 0 skip (all)
$query->join('nampans', function ($join) use ($nampanId) { $query->join('nampans', function ($join) use ($nampanId) {
$join->on('item_transaksis.posisi_asal', '=', 'nampans.nama') $join->on('item_transaksis.posisi_asal', '=', 'nampans.nama')
->where('nampans.id', $nampanId); ->where('nampans.id', $nampanId);
}); });
} }
} }

View File

@ -33,8 +33,8 @@ class DatabaseSeeder extends Seeder
User::factory(2)->create(); User::factory(2)->create();
Sales::factory(5)->create(); Sales::factory(5)->create();
for ($i=0; $i <= 30; $i++) { for ($i=0; $i < 30; $i++) {
if ($i != 13) { if ($i != 12) {
Nampan::factory()->create([ Nampan::factory()->create([
'nama' => 'A' . ($i + 1) 'nama' => 'A' . ($i + 1)
]); ]);

View File

@ -0,0 +1,206 @@
<template>
<div class="relative" ref="datePickerRef">
<!-- Input Display -->
<div class="flex gap-2 items-center">
<div class="flex-1">
<label v-if="label" class="text-D/80 block text-sm font-medium mb-1">{{ label }}</label>
<div
@click="toggleCalendar"
class="w-full px-3 py-2 bg-A text-D border border-B rounded-md cursor-pointer hover:border-C focus-within:border-C focus-within:ring focus-within:ring-D focus-within:ring-opacity-50 transition-colors"
>
<div class="flex items-center justify-between">
<span v-if="displayText" class="text-sm">{{ displayText }}</span>
<span v-else class="text-sm text-D/60">{{ placeholder }}</span>
<i class="fas fa-calendar-alt text-D/60"></i>
</div>
</div>
<div v-if="errorMessage" class="text-red-500 text-xs mt-1">{{ errorMessage }}</div>
</div>
</div>
<!-- Calendar Popup (inline, no teleport) -->
<div
v-if="showCalendar"
ref="popupRef"
class="absolute z-[9999] bg-A border border-C rounded-lg shadow-xl p-4 min-w-[300px] mt-2"
:class="popupPositionClass"
>
<!-- Manual Date Inputs -->
<div class="mb-4">
<div class="text-sm font-medium text-D mb-2">Pilih Manual</div>
<div class="flex gap-3 items-center">
<div class="flex-1">
<label class="text-xs text-D/80 block mb-1">Dari</label>
<input
type="date"
v-model="tempStartDate"
@input="validateDates"
:max="maxDate"
class="w-full text-xs px-2 py-2 bg-A text-D border border-B rounded focus:border-C focus:outline-none transition-colors"
/>
</div>
<span class="text-D/50 text-sm">s/d</span>
<div class="flex-1">
<label class="text-xs text-D/80 block mb-1">Sampai</label>
<input
type="date"
v-model="tempEndDate"
@input="validateDates"
:min="tempStartDate"
:max="maxDate"
class="w-full text-xs px-2 py-2 bg-A text-D border border-B rounded focus:border-C focus:outline-none transition-colors"
/>
</div>
</div>
<!-- Range Info -->
<div v-if="tempStartDate && tempEndDate" class="text-xs text-D/60 mt-2">
{{ rangeDaysText }} ({{ formatDisplayDate(tempStartDate) }} - {{ formatDisplayDate(tempEndDate) }})
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-between items-center pt-3 border-t border-C">
<button
@click="clearDates"
class="px-3 py-1 text-xs text-D/60 hover:text-D transition-colors"
>
Bersihkan
</button>
<div class="flex gap-2">
<button
@click="cancel"
class="px-4 py-1 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 rounded transition-colors"
>
Batal
</button>
<button
@click="confirm"
:disabled="!isValidRange"
class="px-4 py-1 text-xs bg-C hover:bg-C/80 text-D rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Terapkan
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
const props = defineProps({
modelValue: { type: Object, default: () => ({ start: '', end: '' }) },
label: { type: String, default: 'Pilih Periode' },
placeholder: { type: String, default: 'Pilih rentang tanggal' },
maxDays: { type: Number, default: 31 },
position: { type: String, default: 'left', validator: (v) => ['left', 'right'].includes(v) }
})
const emit = defineEmits(['update:modelValue', 'change'])
const datePickerRef = ref(null)
const showCalendar = ref(false)
const tempStartDate = ref('')
const tempEndDate = ref('')
const errorMessage = ref('')
const maxDate = computed(() => new Date().toISOString().split('T')[0])
const displayText = computed(() => {
if (props.modelValue.start && props.modelValue.end) {
const startFormatted = formatDisplayDate(props.modelValue.start)
const endFormatted = formatDisplayDate(props.modelValue.end)
return props.modelValue.start === props.modelValue.end
? startFormatted
: `${startFormatted} - ${endFormatted}`
}
return ''
})
const isValidRange = computed(() => {
if (!tempStartDate.value || !tempEndDate.value) return false
const start = new Date(tempStartDate.value)
const end = new Date(tempEndDate.value)
if (start > end) return false
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
return diffDays <= props.maxDays
})
const rangeDaysText = computed(() => {
if (!tempStartDate.value || !tempEndDate.value) return ''
const start = new Date(tempStartDate.value)
const end = new Date(tempEndDate.value)
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
return diffDays > props.maxDays
? `⚠️ Maksimal ${props.maxDays} hari`
: `${diffDays} hari`
})
const popupPositionClass = computed(() => props.position === 'right' ? 'right-0' : 'left-0')
const formatDisplayDate = (dateString) => {
const date = new Date(dateString + 'T00:00:00')
return date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' })
}
const toggleCalendar = () => {
showCalendar.value = !showCalendar.value
if (showCalendar.value) {
tempStartDate.value = props.modelValue.start
tempEndDate.value = props.modelValue.end
errorMessage.value = ''
}
}
const validateDates = () => {
errorMessage.value = ''
if (!tempStartDate.value || !tempEndDate.value) return
const start = new Date(tempStartDate.value)
const end = new Date(tempEndDate.value)
if (start > end) {
errorMessage.value = 'Tanggal akhir harus setelah tanggal mulai'
return
}
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
if (diffDays > props.maxDays) {
errorMessage.value = `Maksimal ${props.maxDays} hari`
}
}
const clearDates = () => {
tempStartDate.value = ''
tempEndDate.value = ''
errorMessage.value = ''
}
const cancel = () => {
showCalendar.value = false
errorMessage.value = ''
}
const confirm = () => {
if (isValidRange.value) {
const newValue = { start: tempStartDate.value, end: tempEndDate.value }
emit('update:modelValue', newValue)
emit('change', newValue)
showCalendar.value = false
}
}
const handleClickOutside = (e) => {
if (datePickerRef.value && !datePickerRef.value.contains(e.target)) {
showCalendar.value = false
}
}
onMounted(() => document.addEventListener('click', handleClickOutside))
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
watch(() => props.modelValue, (newValue) => {
if (newValue.start !== tempStartDate.value || newValue.end !== tempEndDate.value) {
tempStartDate.value = newValue.start
tempEndDate.value = newValue.end
}
}, { deep: true })
</script>

View File

@ -3,11 +3,15 @@
<hr class="border-B mb-5" /> <hr class="border-B mb-5" />
<!-- Filter Section --> <!-- Filter Section -->
<div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8"> <div class="flex flex-row my-3 gap-1 md:gap-5 lg:gap-8">
<div class="mb-3 w-full"> <div class="mb-3 w-full">
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label> <DatePicker
<input type="date" v-model="tanggalDipilih" id="pilihTanggal" v-model="dateRange"
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" /> label="Filter Tanggal"
placeholder="Pilih rentang tanggal"
:max-days="31"
@change="handleDateChange"
/>
</div> </div>
<div class="mb-3 w-full"> <div class="mb-3 w-full">
<label class="text-D/80" for="pilihSales">Filter Sales:</label> <label class="text-D/80" for="pilihSales">Filter Sales:</label>
@ -26,26 +30,27 @@
<!-- Export Section --> <!-- Export Section -->
<div class="flex flex-row items-center justify-between mt-5 gap-3"> <div class="flex flex-row items-center justify-between mt-5 gap-3">
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="flex gap-4" v-if="data?.rekap_harian"> <div v-if="loading">
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Item</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_item_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Berat</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_berat_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Pendapatan</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div>
</div>
</div>
<div v-else>
<div class="flex items-center justify-center w-full h-30"> <div class="flex items-center justify-center w-full h-30">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div>
<span class="ml-2 text-gray-600">Memuat data...</span> <span class="ml-2 text-gray-600">Memuat data...</span>
</div> </div>
</div> </div>
<div class="flex gap-4" v-else-if="data?.rekap_interval">
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Item</div>
<div class="font-semibold text-D">{{ data.rekap_interval.total_item_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Berat</div>
<div class="font-semibold text-D">{{ data.rekap_interval.total_berat_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Pendapatan</div>
<div class="font-semibold text-D">{{ data.rekap_interval.total_pendapatan }}</div>
</div>
</div>
<div v-else></div>
<!-- Export Dropdown --> <!-- Export Dropdown -->
<div class="relative w-40" ref="exportDropdownRef"> <div class="relative w-40" ref="exportDropdownRef">
@ -151,19 +156,15 @@
</div> </div>
</div> </div>
</div> </div>
<button @click="njir">njir</button>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'; import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
import InputSelect from './InputSelect.vue'; import InputSelect from './InputSelect.vue';
import InputField from './InputField.vue'; import InputField from './InputField.vue';
import DatePicker from './DatePicker.vue';
import axios from 'axios'; import axios from 'axios';
const njir = ()=>{
console.log(sortedNampan.value);
}
// --- State --- // --- State ---
const isExportOpen = ref(false); const isExportOpen = ref(false);
const exportDropdownRef = ref(null); const exportDropdownRef = ref(null);
@ -175,7 +176,7 @@ const exportOptions = ref([
]); ]);
const exportFormat = ref(null); const exportFormat = ref(null);
const tanggalDipilih = ref(''); const dateRange = ref({ start: '', end: '' });
const data = ref(null); const data = ref(null);
const loading = ref(false); const loading = ref(false);
const loadingExport = ref(false); const loadingExport = ref(false);
@ -301,6 +302,13 @@ const getSortIcon = (column) => {
} }
}; };
const handleDateChange = (newDateRange) => {
console.log('Date range changed:', newDateRange);
// Reset pagination when date changes
pagination.value.current_page = 1;
fetchData(1);
};
const fetchSales = async () => { const fetchSales = async () => {
try { try {
const response = await axios.get('/api/sales', { const response = await axios.get('/api/sales', {
@ -342,12 +350,12 @@ const fetchProduk = async () => {
}; };
const fetchData = async (page = 1) => { const fetchData = async (page = 1) => {
if (!tanggalDipilih.value) return; if (!dateRange.value.start || !dateRange.value.end) return;
loading.value = true; loading.value = true;
pendapatanElements.value = []; pendapatanElements.value = [];
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`; let queryParams = `start_date=${dateRange.value.start}&end_date=${dateRange.value.end}&page=${page}`;
if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`; if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`;
if (produkDipilih.value != 0) queryParams += `&produk_id=${produkDipilih.value}`; if (produkDipilih.value != 0) queryParams += `&produk_id=${produkDipilih.value}`;
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`; if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
@ -398,6 +406,11 @@ const goToPage = (page) => {
}; };
const selectExport = async (option) => { const selectExport = async (option) => {
if (!dateRange.value.start || !dateRange.value.end) {
alert('Silakan pilih rentang tanggal terlebih dahulu');
return;
}
exportFormat.value = option.value; exportFormat.value = option.value;
isExportOpen.value = false; isExportOpen.value = false;
loadingExport.value = true; loadingExport.value = true;
@ -405,7 +418,8 @@ const selectExport = async (option) => {
try { try {
const response = await axios.get(`/api/laporan/export/detail-pernampan`, { const response = await axios.get(`/api/laporan/export/detail-pernampan`, {
params: { params: {
tanggal: tanggalDipilih.value, start_date: dateRange.value.start,
end_date: dateRange.value.end,
format: exportFormat.value, format: exportFormat.value,
page: pagination.value.current_page, page: pagination.value.current_page,
sales_id: salesDipilih.value != 0 ? salesDipilih.value : null, sales_id: salesDipilih.value != 0 ? salesDipilih.value : null,
@ -420,7 +434,7 @@ const selectExport = async (option) => {
const url = window.URL.createObjectURL(new Blob([response.data])); const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a'); const link = document.createElement('a');
const fileName = `laporan_${tanggalDipilih.value}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`; const fileName = `laporan_${dateRange.value.start}_to_${dateRange.value.end}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`;
link.href = url; link.href = url;
link.setAttribute('download', fileName); link.setAttribute('download', fileName);
@ -431,6 +445,7 @@ const selectExport = async (option) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (e) { } catch (e) {
console.error("Gagal mengekspor laporan:", e); console.error("Gagal mengekspor laporan:", e);
alert('Gagal mengekspor laporan. Silakan coba lagi.');
} finally { } finally {
loadingExport.value = false; loadingExport.value = false;
} }
@ -444,8 +459,9 @@ const closeDropdownsOnClickOutside = (event) => {
// --- Lifecycle Hooks --- // --- Lifecycle Hooks ---
onMounted(() => { onMounted(() => {
// Set default date range to today
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
tanggalDipilih.value = today; dateRange.value = { start: today, end: today };
fetchSales(); fetchSales();
fetchProduk(); fetchProduk();
@ -457,9 +473,19 @@ onUnmounted(() => {
document.removeEventListener('click', closeDropdownsOnClickOutside); document.removeEventListener('click', closeDropdownsOnClickOutside);
}); });
// Watch for filter changes // Watch for filter changes (except date range which has its own handler)
watch([tanggalDipilih, salesDipilih, produkDipilih, namaPembeli], () => { watch([salesDipilih, produkDipilih, namaPembeli], () => {
pagination.value.current_page = 1; // Reset to first page when filters change if (dateRange.value.start && dateRange.value.end) {
fetchData(1); pagination.value.current_page = 1; // Reset to first page when filters change
}, { immediate: true }); fetchData(1);
}
});
// Watch for date range changes
watch(dateRange, (newDateRange) => {
if (newDateRange.start && newDateRange.end) {
pagination.value.current_page = 1;
fetchData(1);
}
}, { deep: true, immediate: true });
</script> </script>

View File

@ -2,11 +2,16 @@
<div class="my-6"> <div class="my-6">
<hr class="border-B mb-5" /> <hr class="border-B mb-5" />
<div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8"> <!-- Filter Section -->
<div class="mb-3 w-full min-w-fit"> <div class="flex flex-row my-3 gap-1 md:gap-5 lg:gap-8">
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label> <div class="mb-3 w-full">
<input type="date" v-model="tanggalDipilih" id="pilihTanggal" <DatePicker
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" /> v-model="dateRange"
label="Filter Tanggal"
placeholder="Pilih rentang tanggal"
:max-days="31"
@change="handleDateChange"
/>
</div> </div>
<div class="mb-3 w-full min-w-fit"> <div class="mb-3 w-full min-w-fit">
<label class="text-D/80" for="pilihSales">Filter Sales:</label> <label class="text-D/80" for="pilihSales">Filter Sales:</label>
@ -22,29 +27,32 @@
</div> </div>
</div> </div>
<!-- Export Section -->
<div class="flex flex-row items-center justify-between mt-5 gap-3"> <div class="flex flex-row items-center justify-between mt-5 gap-3">
<div class="flex gap-4" v-if="data?.rekap_harian"> <!-- Summary Cards -->
<div class="bg-A p-3 rounded-md border border-C"> <div v-if="loading">
<div class="text-xs text-D/60">Total Item</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_item_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Berat</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_berat_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Pendapatan</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div>
</div>
</div>
<div v-else>
<div class="flex items-center justify-center w-full h-30"> <div class="flex items-center justify-center w-full h-30">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div>
<span class="ml-2 text-gray-600">Memuat data...</span> <span class="ml-2 text-gray-600">Memuat data...</span>
</div> </div>
</div> </div>
<div class="flex gap-4" v-if="data?.rekap_interval">
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Item</div>
<div class="font-semibold text-D">{{ data.rekap_interval.total_item_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Berat</div>
<div class="font-semibold text-D">{{ data.rekap_interval.total_berat_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Pendapatan</div>
<div class="font-semibold text-D">{{ data.rekap_interval.total_pendapatan }}</div>
</div>
</div>
<div v-else></div>
<!-- Export Dropdown -->
<div class="relative w-40" ref="exportDropdownRef"> <div class="relative w-40" ref="exportDropdownRef">
<button v-if="loadingExport" type="button" <button v-if="loadingExport" type="button"
class="flex items-center w-full px-3 py-2 text-sm text-left bg-C/80 border rounded-md border-C/80" disabled> class="flex items-center w-full px-3 py-2 text-sm text-left bg-C/80 border rounded-md border-C/80" disabled>
@ -66,6 +74,7 @@
</div> </div>
</div> </div>
<!-- Table Section -->
<div class="mt-5 overflow-x-auto"> <div class="mt-5 overflow-x-auto">
<table class="w-full border-collapse border border-C rounded-md"> <table class="w-full border-collapse border border-C rounded-md">
<thead> <thead>
@ -130,6 +139,7 @@
</tbody> </tbody>
</table> </table>
<!-- Pagination -->
<div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4"> <div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4">
<button @click="goToPage(pagination.current_page - 1)" :disabled="pagination.current_page === 1 || loading" <button @click="goToPage(pagination.current_page - 1)" :disabled="pagination.current_page === 1 || loading"
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80"> class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
@ -152,6 +162,7 @@
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'; import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
import InputSelect from './InputSelect.vue'; import InputSelect from './InputSelect.vue';
import InputField from './InputField.vue'; import InputField from './InputField.vue';
import DatePicker from './DatePicker.vue';
import axios from 'axios'; import axios from 'axios';
// --- State --- // --- State ---
@ -165,14 +176,14 @@ const exportOptions = ref([
]); ]);
const exportFormat = ref(null); const exportFormat = ref(null);
const tanggalDipilih = ref(''); const dateRange = ref({ start: '', end: '' });
const data = ref(null); const data = ref(null);
const loading = ref(false); const loading = ref(false);
const loadingExport = ref(false); const loadingExport = ref(false);
// Sorting state // Sorting state
const sortBy = ref(null); const sortBy = ref(null);
const sortOrder = ref('asc'); // 'asc' or 'desc' const sortOrder = ref('asc');
const pagination = ref({ const pagination = ref({
current_page: 1, current_page: 1,
@ -185,12 +196,12 @@ const pendapatanElements = ref([]);
const salesDipilih = ref(0); const salesDipilih = ref(0);
const opsiSales = ref([ const opsiSales = ref([
{ label: 'Semua Sales', value: 0, selected: true }, { label: 'Semua Sales', value: 0 },
]); ]);
const nampanDipilih = ref(0); const nampanDipilih = ref(0);
const opsiNampan = ref([ const opsiNampan = ref([
{ label: 'Semua Nampan', value: 0, selected: true }, { label: 'Semua Nampan', value: 0 },
]); ]);
const namaPembeli = ref(null); const namaPembeli = ref(null);
@ -292,6 +303,13 @@ const getSortIcon = (column) => {
} }
}; };
const handleDateChange = (newDateRange) => {
console.log('Date range changed:', newDateRange);
// Reset pagination when date changes
pagination.value.current_page = 1;
fetchData(1);
};
const fetchSales = async () => { const fetchSales = async () => {
try { try {
const response = await axios.get('/api/sales', { const response = await axios.get('/api/sales', {
@ -334,12 +352,12 @@ const fetchNampan = async () => {
}; };
const fetchData = async (page = 1) => { const fetchData = async (page = 1) => {
if (!tanggalDipilih.value) return; if (!dateRange.value.start || !dateRange.value.end) return;
loading.value = true; loading.value = true;
pendapatanElements.value = []; pendapatanElements.value = [];
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`; let queryParams = `start_date=${dateRange.value.start}&end_date=${dateRange.value.end}&page=${page}`;
if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`; if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`;
if (nampanDipilih.value != 0) queryParams += `&nampan_id=${nampanDipilih.value}`; if (nampanDipilih.value != 0) queryParams += `&nampan_id=${nampanDipilih.value}`;
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`; if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
@ -353,6 +371,7 @@ const fetchData = async (page = 1) => {
data.value = response.data; data.value = response.data;
// Handle pagination data if provided by backend
if (response.data.pagination) { if (response.data.pagination) {
pagination.value = { pagination.value = {
current_page: response.data.pagination.current_page, current_page: response.data.pagination.current_page,
@ -360,12 +379,14 @@ const fetchData = async (page = 1) => {
total: response.data.pagination.total, total: response.data.pagination.total,
}; };
} else { } else {
// Reset pagination if no pagination data
pagination.value = { pagination.value = {
current_page: 1, current_page: 1,
last_page: 1, last_page: 1,
total: response.data.produk ? response.data.produk.length : 0, total: response.data.produk ? response.data.produk.length : 0,
}; };
} }
console.log('Data laporan produk berhasil diambil:', data.value);
} catch (error) { } catch (error) {
console.error('Gagal mengambil data laporan produk:', error); console.error('Gagal mengambil data laporan produk:', error);
data.value = null; data.value = null;
@ -387,14 +408,20 @@ const goToPage = (page) => {
}; };
const selectExport = async (option) => { const selectExport = async (option) => {
if (!dateRange.value.start || !dateRange.value.end) {
alert('Silakan pilih rentang tanggal terlebih dahulu');
return;
}
exportFormat.value = option.value; exportFormat.value = option.value;
isExportOpen.value = false; isExportOpen.value = false;
loadingExport.value = true loadingExport.value = true;
try { try {
const response = await axios.get('/api/laporan/export/detail-perproduk', { const response = await axios.get('/api/laporan/export/detail-perproduk', {
params: { params: {
tanggal: tanggalDipilih.value, start_date: dateRange.value.start,
end_date: dateRange.value.end,
format: exportFormat.value, format: exportFormat.value,
page: pagination.value.current_page, page: pagination.value.current_page,
sales_id: salesDipilih.value != 0 ? salesDipilih.value : null, sales_id: salesDipilih.value != 0 ? salesDipilih.value : null,
@ -409,7 +436,7 @@ const selectExport = async (option) => {
const url = window.URL.createObjectURL(new Blob([response.data])); const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a'); const link = document.createElement('a');
const fileName = `laporan_per_produk_${tanggalDipilih.value}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`; const fileName = `laporan_per_produk_${dateRange.value.start}_to_${dateRange.value.end}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`;
link.href = url; link.href = url;
link.setAttribute('download', fileName); link.setAttribute('download', fileName);
@ -420,8 +447,9 @@ const selectExport = async (option) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (e) { } catch (e) {
console.error("Gagal mengekspor laporan per produk:", e); console.error("Gagal mengekspor laporan per produk:", e);
alert('Gagal mengekspor laporan. Silakan coba lagi.');
} finally { } finally {
loadingExport.value = false loadingExport.value = false;
} }
}; };
@ -433,11 +461,12 @@ const closeDropdownsOnClickOutside = (event) => {
// --- Lifecycle Hooks --- // --- Lifecycle Hooks ---
onMounted(() => { onMounted(() => {
// Set default date range to today
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
tanggalDipilih.value = today; dateRange.value = { start: today, end: today };
fetchSales(); fetchSales();
fetchNampan(); // Changed from fetchProduk to fetchNampan fetchNampan();
document.addEventListener('click', closeDropdownsOnClickOutside); document.addEventListener('click', closeDropdownsOnClickOutside);
}); });
@ -446,9 +475,19 @@ onUnmounted(() => {
document.removeEventListener('click', closeDropdownsOnClickOutside); document.removeEventListener('click', closeDropdownsOnClickOutside);
}); });
// Watch for filter changes // Watch for filter changes (except date range which has its own handler)
watch([tanggalDipilih, salesDipilih, nampanDipilih, namaPembeli], () => { // Changed from produkDipilih to nampanDipilih watch([salesDipilih, nampanDipilih, namaPembeli], () => {
pagination.value.current_page = 1; if (dateRange.value.start && dateRange.value.end) {
fetchData(1); pagination.value.current_page = 1; // Reset to first page when filters change
}, { immediate: true }); fetchData(1);
}
});
// Watch for date range changes
watch(dateRange, (newDateRange) => {
if (newDateRange.start && newDateRange.end) {
pagination.value.current_page = 1;
fetchData(1);
}
}, { deep: true, immediate: true });
</script> </script>

View File

@ -102,7 +102,7 @@
<div class="filter-info"> <div class="filter-info">
<h3>Informasi Filter</h3> <h3>Informasi Filter</h3>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }} <span class="filter-label">Tanggal:</span> {{ $data['filter']['periode'] }}
</div> </div>
@if($data['filter']['nama_sales']) @if($data['filter']['nama_sales'])
<div class="filter-item"> <div class="filter-item">

View File

@ -102,7 +102,7 @@
<div class="filter-info"> <div class="filter-info">
<h3>Informasi Filter</h3> <h3>Informasi Filter</h3>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }} <span class="filter-label">Tanggal:</span> {{ $data['filter']['periode'] }}
</div> </div>
@if($data['filter']['nama_sales']) @if($data['filter']['nama_sales'])
<div class="filter-item"> <div class="filter-item">