[Update] Filter interval laporan
This commit is contained in:
parent
97192bb05a
commit
f71fabdc90
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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']);
|
||||
$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);
|
||||
|
||||
$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,7 +434,7 @@ 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);
|
||||
|
||||
@ -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)
|
||||
]);
|
||||
|
||||
206
resources/js/components/DatePicker.vue
Normal file
206
resources/js/components/DatePicker.vue
Normal 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>
|
||||
@ -3,11 +3,15 @@
|
||||
<hr class="border-B mb-5" />
|
||||
|
||||
<!-- 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">
|
||||
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
|
||||
<input type="date" v-model="tanggalDipilih" id="pilihTanggal"
|
||||
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" />
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
label="Filter Tanggal"
|
||||
placeholder="Pilih rentang tanggal"
|
||||
:max-days="31"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 w-full">
|
||||
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
|
||||
@ -26,26 +30,27 @@
|
||||
<!-- Export Section -->
|
||||
<div class="flex flex-row items-center justify-between mt-5 gap-3">
|
||||
<!-- Summary Cards -->
|
||||
<div class="flex gap-4" v-if="data?.rekap_harian">
|
||||
<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 v-if="loading">
|
||||
<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>
|
||||
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||
</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 -->
|
||||
<div class="relative w-40" ref="exportDropdownRef">
|
||||
@ -151,19 +156,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="njir">njir</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
|
||||
import InputSelect from './InputSelect.vue';
|
||||
import InputField from './InputField.vue';
|
||||
import DatePicker from './DatePicker.vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const njir = ()=>{
|
||||
console.log(sortedNampan.value);
|
||||
|
||||
}
|
||||
// --- State ---
|
||||
const isExportOpen = ref(false);
|
||||
const exportDropdownRef = ref(null);
|
||||
@ -175,7 +176,7 @@ const exportOptions = ref([
|
||||
]);
|
||||
|
||||
const exportFormat = ref(null);
|
||||
const tanggalDipilih = ref('');
|
||||
const dateRange = ref({ start: '', end: '' });
|
||||
const data = ref(null);
|
||||
const loading = 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 () => {
|
||||
try {
|
||||
const response = await axios.get('/api/sales', {
|
||||
@ -342,12 +350,12 @@ const fetchProduk = async () => {
|
||||
};
|
||||
|
||||
const fetchData = async (page = 1) => {
|
||||
if (!tanggalDipilih.value) return;
|
||||
if (!dateRange.value.start || !dateRange.value.end) return;
|
||||
|
||||
loading.value = true;
|
||||
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 (produkDipilih.value != 0) queryParams += `&produk_id=${produkDipilih.value}`;
|
||||
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
||||
@ -398,6 +406,11 @@ const goToPage = (page) => {
|
||||
};
|
||||
|
||||
const selectExport = async (option) => {
|
||||
if (!dateRange.value.start || !dateRange.value.end) {
|
||||
alert('Silakan pilih rentang tanggal terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
exportFormat.value = option.value;
|
||||
isExportOpen.value = false;
|
||||
loadingExport.value = true;
|
||||
@ -405,7 +418,8 @@ const selectExport = async (option) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/laporan/export/detail-pernampan`, {
|
||||
params: {
|
||||
tanggal: tanggalDipilih.value,
|
||||
start_date: dateRange.value.start,
|
||||
end_date: dateRange.value.end,
|
||||
format: exportFormat.value,
|
||||
page: pagination.value.current_page,
|
||||
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 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.setAttribute('download', fileName);
|
||||
@ -431,6 +445,7 @@ const selectExport = async (option) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error("Gagal mengekspor laporan:", e);
|
||||
alert('Gagal mengekspor laporan. Silakan coba lagi.');
|
||||
} finally {
|
||||
loadingExport.value = false;
|
||||
}
|
||||
@ -444,8 +459,9 @@ const closeDropdownsOnClickOutside = (event) => {
|
||||
|
||||
// --- Lifecycle Hooks ---
|
||||
onMounted(() => {
|
||||
// Set default date range to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
tanggalDipilih.value = today;
|
||||
dateRange.value = { start: today, end: today };
|
||||
|
||||
fetchSales();
|
||||
fetchProduk();
|
||||
@ -457,9 +473,19 @@ onUnmounted(() => {
|
||||
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
||||
});
|
||||
|
||||
// Watch for filter changes
|
||||
watch([tanggalDipilih, salesDipilih, produkDipilih, namaPembeli], () => {
|
||||
// Watch for filter changes (except date range which has its own handler)
|
||||
watch([salesDipilih, produkDipilih, namaPembeli], () => {
|
||||
if (dateRange.value.start && dateRange.value.end) {
|
||||
pagination.value.current_page = 1; // Reset to first page when filters change
|
||||
fetchData(1);
|
||||
}, { immediate: true });
|
||||
}
|
||||
});
|
||||
|
||||
// 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>
|
||||
@ -2,11 +2,16 @@
|
||||
<div class="my-6">
|
||||
<hr class="border-B mb-5" />
|
||||
|
||||
<div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8">
|
||||
<div class="mb-3 w-full min-w-fit">
|
||||
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
|
||||
<input type="date" v-model="tanggalDipilih" id="pilihTanggal"
|
||||
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" />
|
||||
<!-- Filter Section -->
|
||||
<div class="flex flex-row my-3 gap-1 md:gap-5 lg:gap-8">
|
||||
<div class="mb-3 w-full">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
label="Filter Tanggal"
|
||||
placeholder="Pilih rentang tanggal"
|
||||
:max-days="31"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 w-full min-w-fit">
|
||||
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
|
||||
@ -22,29 +27,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Section -->
|
||||
<div class="flex flex-row items-center justify-between mt-5 gap-3">
|
||||
<div class="flex gap-4" v-if="data?.rekap_harian">
|
||||
<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>
|
||||
<!-- Summary Cards -->
|
||||
<div v-if="loading">
|
||||
<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>
|
||||
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||
</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">
|
||||
<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>
|
||||
@ -66,6 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div class="mt-5 overflow-x-auto">
|
||||
<table class="w-full border-collapse border border-C rounded-md">
|
||||
<thead>
|
||||
@ -130,6 +139,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<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"
|
||||
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 InputSelect from './InputSelect.vue';
|
||||
import InputField from './InputField.vue';
|
||||
import DatePicker from './DatePicker.vue';
|
||||
import axios from 'axios';
|
||||
|
||||
// --- State ---
|
||||
@ -165,14 +176,14 @@ const exportOptions = ref([
|
||||
]);
|
||||
|
||||
const exportFormat = ref(null);
|
||||
const tanggalDipilih = ref('');
|
||||
const dateRange = ref({ start: '', end: '' });
|
||||
const data = ref(null);
|
||||
const loading = ref(false);
|
||||
const loadingExport = ref(false);
|
||||
|
||||
// Sorting state
|
||||
const sortBy = ref(null);
|
||||
const sortOrder = ref('asc'); // 'asc' or 'desc'
|
||||
const sortOrder = ref('asc');
|
||||
|
||||
const pagination = ref({
|
||||
current_page: 1,
|
||||
@ -185,12 +196,12 @@ const pendapatanElements = ref([]);
|
||||
|
||||
const salesDipilih = ref(0);
|
||||
const opsiSales = ref([
|
||||
{ label: 'Semua Sales', value: 0, selected: true },
|
||||
{ label: 'Semua Sales', value: 0 },
|
||||
]);
|
||||
|
||||
const nampanDipilih = ref(0);
|
||||
const opsiNampan = ref([
|
||||
{ label: 'Semua Nampan', value: 0, selected: true },
|
||||
{ label: 'Semua Nampan', value: 0 },
|
||||
]);
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const response = await axios.get('/api/sales', {
|
||||
@ -334,12 +352,12 @@ const fetchNampan = async () => {
|
||||
};
|
||||
|
||||
const fetchData = async (page = 1) => {
|
||||
if (!tanggalDipilih.value) return;
|
||||
if (!dateRange.value.start || !dateRange.value.end) return;
|
||||
|
||||
loading.value = true;
|
||||
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 (nampanDipilih.value != 0) queryParams += `&nampan_id=${nampanDipilih.value}`;
|
||||
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
||||
@ -353,6 +371,7 @@ const fetchData = async (page = 1) => {
|
||||
|
||||
data.value = response.data;
|
||||
|
||||
// Handle pagination data if provided by backend
|
||||
if (response.data.pagination) {
|
||||
pagination.value = {
|
||||
current_page: response.data.pagination.current_page,
|
||||
@ -360,12 +379,14 @@ const fetchData = async (page = 1) => {
|
||||
total: response.data.pagination.total,
|
||||
};
|
||||
} else {
|
||||
// Reset pagination if no pagination data
|
||||
pagination.value = {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
total: response.data.produk ? response.data.produk.length : 0,
|
||||
};
|
||||
}
|
||||
console.log('Data laporan produk berhasil diambil:', data.value);
|
||||
} catch (error) {
|
||||
console.error('Gagal mengambil data laporan produk:', error);
|
||||
data.value = null;
|
||||
@ -387,14 +408,20 @@ const goToPage = (page) => {
|
||||
};
|
||||
|
||||
const selectExport = async (option) => {
|
||||
if (!dateRange.value.start || !dateRange.value.end) {
|
||||
alert('Silakan pilih rentang tanggal terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
exportFormat.value = option.value;
|
||||
isExportOpen.value = false;
|
||||
loadingExport.value = true
|
||||
loadingExport.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/laporan/export/detail-perproduk', {
|
||||
params: {
|
||||
tanggal: tanggalDipilih.value,
|
||||
start_date: dateRange.value.start,
|
||||
end_date: dateRange.value.end,
|
||||
format: exportFormat.value,
|
||||
page: pagination.value.current_page,
|
||||
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 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.setAttribute('download', fileName);
|
||||
@ -420,8 +447,9 @@ const selectExport = async (option) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error("Gagal mengekspor laporan per produk:", e);
|
||||
alert('Gagal mengekspor laporan. Silakan coba lagi.');
|
||||
} finally {
|
||||
loadingExport.value = false
|
||||
loadingExport.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -433,11 +461,12 @@ const closeDropdownsOnClickOutside = (event) => {
|
||||
|
||||
// --- Lifecycle Hooks ---
|
||||
onMounted(() => {
|
||||
// Set default date range to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
tanggalDipilih.value = today;
|
||||
dateRange.value = { start: today, end: today };
|
||||
|
||||
fetchSales();
|
||||
fetchNampan(); // Changed from fetchProduk to fetchNampan
|
||||
fetchNampan();
|
||||
|
||||
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||
});
|
||||
@ -446,9 +475,19 @@ onUnmounted(() => {
|
||||
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
||||
});
|
||||
|
||||
// Watch for filter changes
|
||||
watch([tanggalDipilih, salesDipilih, nampanDipilih, namaPembeli], () => { // Changed from produkDipilih to nampanDipilih
|
||||
// Watch for filter changes (except date range which has its own handler)
|
||||
watch([salesDipilih, nampanDipilih, namaPembeli], () => {
|
||||
if (dateRange.value.start && dateRange.value.end) {
|
||||
pagination.value.current_page = 1; // Reset to first page when filters change
|
||||
fetchData(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for date range changes
|
||||
watch(dateRange, (newDateRange) => {
|
||||
if (newDateRange.start && newDateRange.end) {
|
||||
pagination.value.current_page = 1;
|
||||
fetchData(1);
|
||||
}, { immediate: true });
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
</script>
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
<div class="filter-info">
|
||||
<h3>Informasi Filter</h3>
|
||||
<div class="filter-item">
|
||||
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }}
|
||||
<span class="filter-label">Tanggal:</span> {{ $data['filter']['periode'] }}
|
||||
</div>
|
||||
@if($data['filter']['nama_sales'])
|
||||
<div class="filter-item">
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
<div class="filter-info">
|
||||
<h3>Informasi Filter</h3>
|
||||
<div class="filter-item">
|
||||
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }}
|
||||
<span class="filter-label">Tanggal:</span> {{ $data['filter']['periode'] }}
|
||||
</div>
|
||||
@if($data['filter']['nama_sales'])
|
||||
<div class="filter-item">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user