Compare commits
2 Commits
156671a21b
...
3313ae13c8
Author | SHA1 | Date | |
---|---|---|---|
|
3313ae13c8 | ||
|
fc21772679 |
@ -11,29 +11,36 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|||||||
class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, WithStyles
|
class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, WithStyles
|
||||||
{
|
{
|
||||||
private $data;
|
private $data;
|
||||||
private $page;
|
|
||||||
|
|
||||||
public function __construct($data, $page = 1)
|
public function __construct($data)
|
||||||
{
|
{
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
$this->page = $page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function collection()
|
public function collection()
|
||||||
{
|
{
|
||||||
$collection = collect();
|
$collection = collect();
|
||||||
|
|
||||||
|
// Add individual nampan data
|
||||||
if (isset($this->data['nampan'])) {
|
if (isset($this->data['nampan'])) {
|
||||||
foreach ($this->data['nampan'] as $item) {
|
foreach ($this->data['nampan'] as $item) {
|
||||||
$collection->push([
|
$collection->push([
|
||||||
'Nama Nampan' => $item['nama_nampan'],
|
$item['nama_nampan'],
|
||||||
'Jumlah Item Terjual' => $item['jumlah_item_terjual'],
|
$item['jumlah_item_terjual'],
|
||||||
'Berat Terjual' => $item['berat_terjual'],
|
$item['berat_terjual'],
|
||||||
'Pendapatan' => $item['pendapatan'],
|
$item['pendapatan'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isset($this->data['rekap_harian'])) {
|
||||||
|
$rekap = $this->data['rekap_harian'];
|
||||||
|
$collection->push([
|
||||||
|
'REKAP TOTAL',
|
||||||
|
$rekap['total_item_terjual'],
|
||||||
|
$rekap['total_berat_terjual'],
|
||||||
|
$rekap['total_pendapatan'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
return $collection;
|
return $collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +48,7 @@ class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, Wit
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'Nama Nampan',
|
'Nama Nampan',
|
||||||
'Jumlah Item Terjual',
|
'Jumlah Item Terjual',
|
||||||
'Berat Terjual',
|
'Berat Terjual',
|
||||||
'Pendapatan'
|
'Pendapatan'
|
||||||
];
|
];
|
||||||
@ -51,13 +58,26 @@ class DetailNampanExport implements FromCollection, WithHeadings, WithTitle, Wit
|
|||||||
{
|
{
|
||||||
$filterInfo = $this->data['filter'] ?? [];
|
$filterInfo = $this->data['filter'] ?? [];
|
||||||
$tanggal = $filterInfo['tanggal'] ?? 'Unknown';
|
$tanggal = $filterInfo['tanggal'] ?? 'Unknown';
|
||||||
return "Detail Nampan {$tanggal} - Hal {$this->page}";
|
return "Detail Nampan {$tanggal}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function styles(Worksheet $sheet)
|
public function styles(Worksheet $sheet)
|
||||||
{
|
{
|
||||||
return [
|
$styles = [
|
||||||
1 => ['font' => ['bold' => true]],
|
1 => ['font' => ['bold' => true]], // Header row
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Style for recap row if exists
|
||||||
|
if (isset($this->data['rekap_harian'])) {
|
||||||
|
$styles[2] = [
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['argb' => 'FFE2E3E5'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $styles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,25 +11,38 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|||||||
class DetailProdukExport implements FromCollection, WithHeadings, WithTitle, WithStyles
|
class DetailProdukExport implements FromCollection, WithHeadings, WithTitle, WithStyles
|
||||||
{
|
{
|
||||||
private $data;
|
private $data;
|
||||||
private $page;
|
|
||||||
|
|
||||||
public function __construct($data, $page = 1)
|
public function __construct($data)
|
||||||
{
|
{
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
$this->page = $page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function collection()
|
public function collection()
|
||||||
{
|
{
|
||||||
$collection = collect();
|
$collection = collect();
|
||||||
|
|
||||||
|
// Add summary row first
|
||||||
|
if (isset($this->data['rekap_harian'])) {
|
||||||
|
$rekap = $this->data['rekap_harian'];
|
||||||
|
$collection->push([
|
||||||
|
'REKAP TOTAL',
|
||||||
|
$rekap['total_item_terjual'],
|
||||||
|
$rekap['total_berat_terjual'],
|
||||||
|
$rekap['total_pendapatan'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add empty row separator
|
||||||
|
$collection->push(['', '', '', '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add individual produk data
|
||||||
if (isset($this->data['produk'])) {
|
if (isset($this->data['produk'])) {
|
||||||
foreach ($this->data['produk'] as $item) {
|
foreach ($this->data['produk'] as $item) {
|
||||||
$collection->push([
|
$collection->push([
|
||||||
'Nama Produk' => $item['nama_produk'],
|
$item['nama_produk'],
|
||||||
'Jumlah Item Terjual' => $item['jumlah_item_terjual'],
|
$item['jumlah_item_terjual'],
|
||||||
'Berat Terjual' => $item['berat_terjual'],
|
$item['berat_terjual'],
|
||||||
'Pendapatan' => $item['pendapatan'],
|
$item['pendapatan'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,13 +64,26 @@ class DetailProdukExport implements FromCollection, WithHeadings, WithTitle, Wit
|
|||||||
{
|
{
|
||||||
$filterInfo = $this->data['filter'] ?? [];
|
$filterInfo = $this->data['filter'] ?? [];
|
||||||
$tanggal = $filterInfo['tanggal'] ?? 'Unknown';
|
$tanggal = $filterInfo['tanggal'] ?? 'Unknown';
|
||||||
return "Detail Produk {$tanggal} - Hal {$this->page}";
|
return "Detail Produk {$tanggal}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function styles(Worksheet $sheet)
|
public function styles(Worksheet $sheet)
|
||||||
{
|
{
|
||||||
return [
|
$styles = [
|
||||||
1 => ['font' => ['bold' => true]],
|
1 => ['font' => ['bold' => true]], // Header row
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Style for recap row if exists
|
||||||
|
if (isset($this->data['rekap_harian'])) {
|
||||||
|
$styles[2] = [
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['argb' => 'FFE2E3E5'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $styles;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -51,7 +51,7 @@ class LaporanController extends Controller
|
|||||||
return response()->json($data);
|
return response()->json($data);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error in detailPerProduk method: ' . $e->getMessage());
|
Log::error('Error in detail PerProduk method: ' . $e->getMessage());
|
||||||
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data produk'], 500);
|
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data produk'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,4 +84,40 @@ class LaporanController extends Controller
|
|||||||
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function exportDetailNampan(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->laporanService->exportPerNampan($request->validate([
|
||||||
|
'tanggal' => 'nullable|string',
|
||||||
|
'sales_id' => 'nullable|integer|exists:sales,id',
|
||||||
|
'produk_id' => 'nullable|integer|exists:produk,id',
|
||||||
|
'nama_pembeli' => 'nullable|string|max:255',
|
||||||
|
'format' => 'required|string|in:pdf,xlsx,csv',
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
]));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in exprot per nampan method: ' . $e->getMessage());
|
||||||
|
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportDetailProduk(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->laporanService->exportPerProduk($request->validate([
|
||||||
|
'tanggal' => 'nullable|string',
|
||||||
|
'sales_id' => 'nullable|integer|exists:sales,id',
|
||||||
|
'nampan_id' => 'nullable|integer|exists:nampan,id',
|
||||||
|
'nama_pembeli' => 'nullable|string|max:255',
|
||||||
|
'format' => 'required|string|in:pdf,xlsx,csv',
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
]));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in exprot per nampan method: ' . $e->getMessage());
|
||||||
|
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,58 +41,91 @@ class LaporanService
|
|||||||
public function getRingkasan(string $filter, int $page)
|
public function getRingkasan(string $filter, int $page)
|
||||||
{
|
{
|
||||||
$cacheKey = "laporan_ringkasan_{$filter}_page_{$page}";
|
$cacheKey = "laporan_ringkasan_{$filter}_page_{$page}";
|
||||||
|
|
||||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filter, $page) {
|
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filter, $page) {
|
||||||
$allSalesNames = $this->getAllSalesNames();
|
$allSalesNames = $this->getAllSalesNames();
|
||||||
|
|
||||||
if ($filter === 'hari') {
|
if ($filter === 'hari') {
|
||||||
return $this->processLaporanHarian($allSalesNames, $page, true);
|
return $this->processLaporanHarian($allSalesNames, $page, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->processLaporanBulanan($allSalesNames, $page, true);
|
return $this->processLaporanBulanan($allSalesNames, $page, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get paginated sales detail aggregated by product.
|
||||||
|
*
|
||||||
|
* @param array $params Filter parameters (tanggal, sales_id, nampan_id, nama_pembeli, page, per_page)
|
||||||
|
* @return array Report data structure
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
public function getDetailPerProduk(array $params)
|
public function getDetailPerProduk(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = Carbon::parse($params['tanggal']);
|
$tanggal = Carbon::parse($params['tanggal']);
|
||||||
$page = $params['page'] ?? 1;
|
$page = $params['page'] ?? 1;
|
||||||
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
// Validasi nampan_id jika ada
|
// --- Step 1: Calculate overall totals for all filtered items ---
|
||||||
if (isset($params['nampan_id']) && $params['nampan_id'] != 0) {
|
// We need a separate query for totals that is not affected by pagination.
|
||||||
if (!Nampan::where('id', $params['nampan_id'])->exists()) {
|
$totalsQuery = $this->buildBaseItemQuery($tanggal);
|
||||||
throw new \Exception('Nampan tidak ditemukan');
|
$this->applyFilters($totalsQuery, $params);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$produkTerjualQuery = $this->buildBaseItemQuery($tanggal);
|
$totalsResult = $totalsQuery->select(
|
||||||
$this->applyFilters($produkTerjualQuery, $params);
|
DB::raw('COUNT(item_transaksis.id) as total_item_terjual'),
|
||||||
|
DB::raw('COALESCE(SUM(produks.berat), 0) as total_berat_terjual'),
|
||||||
|
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan')
|
||||||
|
)->first();
|
||||||
|
|
||||||
$produkTerjual = $produkTerjualQuery
|
$rekapHarian = [
|
||||||
|
'total_item_terjual' => (int) $totalsResult->total_item_terjual,
|
||||||
|
'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual), // Assuming formatting helper
|
||||||
|
'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan), // Assuming formatting helper
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Step 2: Build the filtered sales data subquery ---
|
||||||
|
$salesSubQuery = $this->buildBaseItemQuery($tanggal)
|
||||||
->select(
|
->select(
|
||||||
'produks.id as id_produk',
|
'produks.id as id_produk',
|
||||||
'produks.nama as nama_produk',
|
|
||||||
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
||||||
DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'),
|
DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'),
|
||||||
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan')
|
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan')
|
||||||
)
|
)
|
||||||
->groupBy('produks.id', 'produks.nama')
|
->groupBy('produks.id');
|
||||||
->get()
|
// Apply filters to the subquery
|
||||||
->keyBy('id_produk');
|
$this->applyFilters($salesSubQuery, $params);
|
||||||
|
|
||||||
$totals = $this->helper->calculateTotals($produkTerjual);
|
// --- Step 3: Fetch paginated products and LEFT JOIN the sales data subquery ---
|
||||||
$semuaProdukPaginated = Produk::select('id', 'nama')
|
$semuaProdukPaginated = Produk::select(
|
||||||
->orderBy('nama')
|
'produks.id',
|
||||||
|
'produks.nama as nama_produk',
|
||||||
|
'sales_data.jumlah_item_terjual',
|
||||||
|
'sales_data.berat_terjual',
|
||||||
|
'sales_data.pendapatan'
|
||||||
|
)
|
||||||
|
->leftJoinSub($salesSubQuery, 'sales_data', function ($join) {
|
||||||
|
$join->on('produks.id', '=', 'sales_data.id_produk');
|
||||||
|
})
|
||||||
|
->orderBy('produks.nama')
|
||||||
->paginate($perPage, ['*'], 'page', $page);
|
->paginate($perPage, ['*'], 'page', $page);
|
||||||
|
|
||||||
$detailItem = $this->helper->mapProductsWithSalesData($semuaProdukPaginated, $produkTerjual);
|
// --- Step 4: Map results for final presentation ---
|
||||||
|
$detailItem = $semuaProdukPaginated->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'nama_produk' => $item->nama_produk,
|
||||||
|
'jumlah_item_terjual' => $item->jumlah_item_terjual ? (int) $item->jumlah_item_terjual : 0, // Use 0 or default display value
|
||||||
|
'berat_terjual' => $item->berat_terjual ? $this->helper->formatWeight($item->berat_terjual) : '-',
|
||||||
|
'pendapatan' => $item->pendapatan ? $this->helper->formatCurrency($item->pendapatan) : '-',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Step 5: Assemble final response ---
|
||||||
$filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params);
|
$filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'filter' => $filterInfo,
|
'filter' => $filterInfo,
|
||||||
'rekap_harian' => $totals,
|
'rekap_harian' => $rekapHarian,
|
||||||
'produk' => $detailItem->values(),
|
'produk' => $detailItem,
|
||||||
'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated),
|
'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -137,11 +170,11 @@ class LaporanService
|
|||||||
$filter = $params['filter'];
|
$filter = $params['filter'];
|
||||||
$format = $params['format'];
|
$format = $params['format'];
|
||||||
$page = $params['page'] ?? 1;
|
$page = $params['page'] ?? 1;
|
||||||
|
|
||||||
$allSalesNames = $this->getAllSalesNames();
|
$allSalesNames = $this->getAllSalesNames();
|
||||||
|
|
||||||
if ($filter === 'hari') {
|
if ($filter === 'hari') {
|
||||||
// Tar kalau mau ubah eksport laporan setiap hari, param ke-3 jadiin false #Bagas
|
// Tar kalau mau ubah eksport laporan setiap hari, param ke-3 jadiin false #Bagas
|
||||||
$data = $this->processLaporanHarian($allSalesNames, $page, true);
|
$data = $this->processLaporanHarian($allSalesNames, $page, true);
|
||||||
} else {
|
} else {
|
||||||
$data = $this->processLaporanBulanan($allSalesNames, $page, true);
|
$data = $this->processLaporanBulanan($allSalesNames, $page, true);
|
||||||
@ -156,13 +189,175 @@ class LaporanService
|
|||||||
'data' => $viewData,
|
'data' => $viewData,
|
||||||
'filter' => $filter
|
'filter' => $filter
|
||||||
]);
|
]);
|
||||||
$pdf->setPaper('a4', 'landscape');
|
$pdf->setPaper('a4', 'potrait');
|
||||||
return $pdf->download($fileName);
|
return $pdf->download($fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Excel::download(new RingkasanExport($data, $page), $fileName);
|
return Excel::download(new RingkasanExport($data, $page), $fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method baru untuk export per produk
|
||||||
|
public function exportPerProduk(array $params)
|
||||||
|
{
|
||||||
|
$tanggal = $params['tanggal'];
|
||||||
|
$format = $params['format'];
|
||||||
|
|
||||||
|
// Get all data tanpa pagination karena untuk export
|
||||||
|
$allParams = $params;
|
||||||
|
unset($allParams['page'], $allParams['per_page']);
|
||||||
|
|
||||||
|
// Get data dengan semua produk (tanpa pagination)
|
||||||
|
$data = $this->getDetailPerProdukForExport($allParams);
|
||||||
|
|
||||||
|
$fileName = "laporan_per_produk_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}";
|
||||||
|
|
||||||
|
if ($format === 'pdf') {
|
||||||
|
$pdf = PDF::loadView('exports.perproduk_pdf', [
|
||||||
|
'data' => $data,
|
||||||
|
'title' => 'Laporan Detail Per Produk'
|
||||||
|
]);
|
||||||
|
$pdf->setPaper('a4', 'potrait');
|
||||||
|
return $pdf->download($fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Excel::download(new DetailProdukExport($data), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportPerNampan(array $params)
|
||||||
|
{
|
||||||
|
$tanggal = $params['tanggal'];
|
||||||
|
$format = $params['format'];
|
||||||
|
|
||||||
|
// Get all data tanpa pagination karena untuk export
|
||||||
|
$allParams = $params;
|
||||||
|
unset($allParams['page'], $allParams['per_page']);
|
||||||
|
|
||||||
|
// Get data dengan semua nampan (tanpa pagination)
|
||||||
|
$data = $this->getDetailPerNampanForExport($allParams);
|
||||||
|
|
||||||
|
$fileName = "laporan_per_nampan_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}";
|
||||||
|
|
||||||
|
if ($format === 'pdf') {
|
||||||
|
$pdf = PDF::loadView('exports.pernampan_pdf', [
|
||||||
|
'data' => $data,
|
||||||
|
'title' => 'Laporan Detail Per Nampan'
|
||||||
|
]);
|
||||||
|
$pdf->setPaper('a4', 'potrait');
|
||||||
|
return $pdf->download($fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Excel::download(new DetailNampanExport($data), $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method untuk get data produk tanpa pagination (untuk export)
|
||||||
|
private function getDetailPerProdukForExport(array $params)
|
||||||
|
{
|
||||||
|
$tanggal = Carbon::parse($params['tanggal']);
|
||||||
|
|
||||||
|
$produkTerjualQuery = $this->buildBaseItemQuery($tanggal);
|
||||||
|
$this->applyFilters($produkTerjualQuery, $params);
|
||||||
|
|
||||||
|
$produkTerjual = $produkTerjualQuery
|
||||||
|
->select(
|
||||||
|
'produks.id as id_produk',
|
||||||
|
'produks.nama as nama_produk',
|
||||||
|
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
||||||
|
DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'),
|
||||||
|
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan')
|
||||||
|
)
|
||||||
|
->groupBy('produks.id', 'produks.nama')
|
||||||
|
->get()
|
||||||
|
->keyBy('id_produk');
|
||||||
|
|
||||||
|
$totals = $this->helper->calculateTotals($produkTerjual);
|
||||||
|
|
||||||
|
// Get all products without pagination
|
||||||
|
$semuaProduk = Produk::select('id', 'nama')->orderBy('nama')->get();
|
||||||
|
|
||||||
|
$detailItem = collect($semuaProduk)->map(function ($item) use ($produkTerjual) {
|
||||||
|
if ($produkTerjual->has($item->id)) {
|
||||||
|
$dataTerjual = $produkTerjual->get($item->id);
|
||||||
|
return [
|
||||||
|
'nama_produk' => $item->nama,
|
||||||
|
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual,
|
||||||
|
'berat_terjual' => $this->helper->formatWeight($dataTerjual->berat_terjual),
|
||||||
|
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama_produk' => $item->nama,
|
||||||
|
'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
||||||
|
'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
||||||
|
'pendapatan' => LaporanHelper::DEFAULT_DISPLAY,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'filter' => $filterInfo,
|
||||||
|
'rekap_harian' => $totals,
|
||||||
|
'produk' => $detailItem->values(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method untuk get data nampan tanpa pagination (untuk export)
|
||||||
|
private function getDetailPerNampanForExport(array $params)
|
||||||
|
{
|
||||||
|
$tanggal = Carbon::parse($params['tanggal']);
|
||||||
|
|
||||||
|
$nampanTerjualQuery = $this->buildBaseItemQuery($tanggal);
|
||||||
|
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
||||||
|
|
||||||
|
$nampanTerjual = $nampanTerjualQuery
|
||||||
|
->leftJoin('nampans', 'items.id_nampan', '=', 'nampans.id')
|
||||||
|
->select(
|
||||||
|
DB::raw('COALESCE(items.id_nampan, 0) as id_nampan'),
|
||||||
|
DB::raw('COALESCE(nampans.nama, "Brankas") as nama_nampan'),
|
||||||
|
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
||||||
|
DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'),
|
||||||
|
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan')
|
||||||
|
)
|
||||||
|
->groupBy('id_nampan', 'nama_nampan')
|
||||||
|
->get()
|
||||||
|
->keyBy('id_nampan');
|
||||||
|
|
||||||
|
$totals = $this->helper->calculateTotals($nampanTerjual);
|
||||||
|
|
||||||
|
// Get all nampan without pagination
|
||||||
|
$semuaNampan = Nampan::select('id', 'nama')->orderBy('nama')->get();
|
||||||
|
$brankasEntry = (object) ['id' => 0, 'nama' => 'Brankas'];
|
||||||
|
$semuaNampanCollection = $semuaNampan->prepend($brankasEntry);
|
||||||
|
|
||||||
|
$detailItem = $semuaNampanCollection->map(function ($item) use ($nampanTerjual) {
|
||||||
|
if ($nampanTerjual->has($item->id)) {
|
||||||
|
$dataTerjual = $nampanTerjual->get($item->id);
|
||||||
|
return [
|
||||||
|
'nama_nampan' => $item->nama,
|
||||||
|
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual,
|
||||||
|
'berat_terjual' => $this->helper->formatWeight($dataTerjual->berat_terjual),
|
||||||
|
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama_nampan' => $item->nama,
|
||||||
|
'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
||||||
|
'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
||||||
|
'pendapatan' => LaporanHelper::DEFAULT_DISPLAY,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'filter' => $filterInfo,
|
||||||
|
'rekap_harian' => $totals,
|
||||||
|
'nampan' => $detailItem->values(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function getAllSalesNames(): Collection
|
private function getAllSalesNames(): Collection
|
||||||
{
|
{
|
||||||
return Cache::remember('all_sales_names', self::CACHE_TTL, function () {
|
return Cache::remember('all_sales_names', self::CACHE_TTL, function () {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="my-6">
|
<div class="my-6">
|
||||||
<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 overflow-x-auto gap-1 md:gap-5 lg:gap-8">
|
||||||
<div class="mb-3 w-full">
|
<div class="mb-3 w-full">
|
||||||
@ -40,10 +40,20 @@
|
|||||||
<div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div>
|
<div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Export Dropdown -->
|
<!-- Export Dropdown -->
|
||||||
<div class="relative w-40" ref="exportDropdownRef">
|
<div class="relative w-40" ref="exportDropdownRef">
|
||||||
<button @click="isExportOpen = !isExportOpen" 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>
|
||||||
|
<i class="fas fa-spinner fa-spin mr-2"></i> Memproses...
|
||||||
|
</button>
|
||||||
|
<button v-else @click="isExportOpen = !isExportOpen" type="button"
|
||||||
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
||||||
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
|
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
@ -65,29 +75,29 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-C text-D rounded-t-md">
|
<tr class="bg-C text-D rounded-t-md">
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('nama_nampan')"
|
<button @click="handleSort('nama_nampan')"
|
||||||
class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors">
|
class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors">
|
||||||
<span>Nama Nampan</span>
|
<span>Nama Nampan</span>
|
||||||
<i :class="getSortIcon('nama_nampan')" class="ml-2"></i>
|
<i :class="getSortIcon('nama_nampan')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('jumlah_item_terjual')"
|
<button @click="handleSort('jumlah_item_terjual')"
|
||||||
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
||||||
<span>Item Terjual</span>
|
<span>Item Terjual</span>
|
||||||
<i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i>
|
<i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('berat_terjual')"
|
<button @click="handleSort('berat_terjual')"
|
||||||
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
||||||
<span>Total Berat</span>
|
<span>Total Berat</span>
|
||||||
<i :class="getSortIcon('berat_terjual')" class="ml-2"></i>
|
<i :class="getSortIcon('berat_terjual')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('pendapatan')"
|
<button @click="handleSort('pendapatan')"
|
||||||
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
||||||
<span>Total Pendapatan</span>
|
<span>Total Pendapatan</span>
|
||||||
<i :class="getSortIcon('pendapatan')" class="ml-2"></i>
|
<i :class="getSortIcon('pendapatan')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -113,9 +123,8 @@
|
|||||||
<td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td>
|
<td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td>
|
||||||
<td class="border-x border-C px-3 py-2">
|
<td class="border-x border-C px-3 py-2">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div :ref="el => { if (el) pendapatanElements.push(el) }"
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle"
|
||||||
:style="pendapatanStyle"
|
:class="item.pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
:class="item.pendapatan == '-' ? 'text-center' : 'text-right'">
|
|
||||||
{{ item.pendapatan }}
|
{{ item.pendapatan }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,17 +136,16 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- 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)"
|
<button @click="goToPage(pagination.current_page - 1)" :disabled="pagination.current_page === 1 || loading"
|
||||||
: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">
|
|
||||||
Sebelumnya
|
Sebelumnya
|
||||||
</button>
|
</button>
|
||||||
<span class="text-sm text-D">
|
<span class="text-sm text-D">
|
||||||
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
|
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
|
||||||
</span>
|
</span>
|
||||||
<button @click="goToPage(pagination.current_page + 1)"
|
<button @click="goToPage(pagination.current_page + 1)"
|
||||||
:disabled="(pagination.current_page === pagination.last_page) || loading"
|
:disabled="(pagination.current_page === pagination.last_page) || 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">
|
||||||
Berikutnya
|
Berikutnya
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -157,7 +165,7 @@ const exportDropdownRef = ref(null);
|
|||||||
|
|
||||||
const exportOptions = ref([
|
const exportOptions = ref([
|
||||||
{ value: 'pdf', label: 'Pdf' },
|
{ value: 'pdf', label: 'Pdf' },
|
||||||
{ value: 'xls', label: 'Excel' },
|
{ value: 'xlsx', label: 'Excel' },
|
||||||
{ value: 'csv', label: 'Csv' }
|
{ value: 'csv', label: 'Csv' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -165,6 +173,7 @@ const exportFormat = ref(null);
|
|||||||
const tanggalDipilih = ref('');
|
const tanggalDipilih = ref('');
|
||||||
const data = ref(null);
|
const data = ref(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const loadingExport = ref(false);
|
||||||
|
|
||||||
// Sorting state
|
// Sorting state
|
||||||
const sortBy = ref(null);
|
const sortBy = ref(null);
|
||||||
@ -255,7 +264,7 @@ watch(nampan, async (newValue) => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
pendapatanElements.value = [];
|
pendapatanElements.value = [];
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
pendapatanElements.value.forEach(el => {
|
pendapatanElements.value.forEach(el => {
|
||||||
if (el && el.scrollWidth > maxWidth) {
|
if (el && el.scrollWidth > maxWidth) {
|
||||||
@ -282,7 +291,7 @@ const getSortIcon = (column) => {
|
|||||||
if (sortBy.value !== column) {
|
if (sortBy.value !== column) {
|
||||||
return 'fas fa-sort text-D/40'; // Default sort icon
|
return 'fas fa-sort text-D/40'; // Default sort icon
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortOrder.value === 'asc') {
|
if (sortOrder.value === 'asc') {
|
||||||
return 'fas fa-sort-up text-D'; // Ascending
|
return 'fas fa-sort-up text-D'; // Ascending
|
||||||
} else {
|
} else {
|
||||||
@ -299,7 +308,7 @@ const fetchSales = async () => {
|
|||||||
});
|
});
|
||||||
const salesData = response.data;
|
const salesData = response.data;
|
||||||
opsiSales.value = [
|
opsiSales.value = [
|
||||||
{ label: 'Semua Sales', value: null },
|
{ label: 'Semua Sales', value: null },
|
||||||
...salesData.map(sales => ({
|
...salesData.map(sales => ({
|
||||||
label: sales.nama,
|
label: sales.nama,
|
||||||
value: sales.id,
|
value: sales.id,
|
||||||
@ -319,7 +328,7 @@ const fetchProduk = async () => {
|
|||||||
});
|
});
|
||||||
const produkData = response.data;
|
const produkData = response.data;
|
||||||
opsiProduk.value = [
|
opsiProduk.value = [
|
||||||
{ label: 'Semua Produk', value: null },
|
{ label: 'Semua Produk', value: null },
|
||||||
...produkData.map(produk => ({
|
...produkData.map(produk => ({
|
||||||
label: produk.nama,
|
label: produk.nama,
|
||||||
value: produk.id,
|
value: produk.id,
|
||||||
@ -342,14 +351,14 @@ const fetchData = async (page = 1) => {
|
|||||||
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/detail-per-nampan?${queryParams}`, {
|
const response = await axios.get(`/api/laporan/detail-per-nampan?${queryParams}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
data.value = response.data;
|
data.value = response.data;
|
||||||
|
|
||||||
// Handle pagination data if provided by backend
|
// Handle pagination data if provided by backend
|
||||||
if (response.data.pagination) {
|
if (response.data.pagination) {
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
@ -385,10 +394,43 @@ const goToPage = (page) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectExport = (option) => {
|
const selectExport = async (option) => {
|
||||||
exportFormat.value = option.value;
|
exportFormat.value = option.value;
|
||||||
isExportOpen.value = false;
|
isExportOpen.value = false;
|
||||||
alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`);
|
loadingExport.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/laporan/export/detail-pernampan', {
|
||||||
|
params: {
|
||||||
|
tanggal: tanggalDipilih.value,
|
||||||
|
sales_id: salesDipilih.value,
|
||||||
|
produk_id: produkDipilih.value,
|
||||||
|
nama_pembeli: namaPembeli.value,
|
||||||
|
format: exportFormat.value,
|
||||||
|
page: pagination.value.current_page,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Gagal mengekspor laporan:", e);
|
||||||
|
} finally {
|
||||||
|
loadingExport.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDropdownsOnClickOutside = (event) => {
|
const closeDropdownsOnClickOutside = (event) => {
|
||||||
@ -404,7 +446,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
fetchSales();
|
fetchSales();
|
||||||
fetchProduk();
|
fetchProduk();
|
||||||
|
|
||||||
document.addEventListener('click', closeDropdownsOnClickOutside);
|
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="my-6">
|
<div class="my-6">
|
||||||
<hr class="border-B mb-5" />
|
<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 overflow-x-auto gap-1 md:gap-5 lg:gap-8">
|
||||||
<div class="mb-3 w-full">
|
<div class="mb-3 w-full min-w-fit">
|
||||||
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
|
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
|
||||||
<input type="date" v-model="tanggalDipilih" id="pilihTanggal"
|
<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" />
|
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" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 w-full">
|
<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>
|
||||||
<InputSelect :options="opsiSales" v-model="salesDipilih" id="pilihSales" />
|
<InputSelect :options="opsiSales" v-model="salesDipilih" id="pilihSales" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 w-full">
|
<div class="mb-3 w-full min-w-fit">
|
||||||
<label class="text-D/80" for="pilihPelanggan">Nama Pembeli:</label>
|
<label class="text-D/80" for="pilihPelanggan">Nama Pembeli:</label>
|
||||||
<InputField placeholder="Nama pelanggan" v-model="namaPembeli" id="pilihPelanggan" />
|
<InputField placeholder="Cari nama pelanggan" v-model="namaPembeli" id="pilihPelanggan" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 w-full">
|
<div class="mb-3 w-full min-w-fit">
|
||||||
<label class="text-D/80" for="pilihNampan">Filter Nampan:</label>
|
<label class="text-D/80" for="pilihNampan">Filter Nampan:</label>
|
||||||
<InputSelect :options="opsiNampan" v-model="nampanDipilih" id="pilihNampan" />
|
<InputSelect :options="opsiNampan" v-model="nampanDipilih" id="pilihNampan" />
|
||||||
</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">
|
||||||
<!-- Summary Cards -->
|
|
||||||
<div class="flex gap-4" v-if="data?.rekap_harian">
|
<div class="flex gap-4" v-if="data?.rekap_harian">
|
||||||
<div class="bg-A p-3 rounded-md border border-C">
|
<div class="bg-A p-3 rounded-md border border-C">
|
||||||
<div class="text-xs text-D/60">Total Item</div>
|
<div class="text-xs text-D/60">Total Item</div>
|
||||||
@ -41,9 +38,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Export Dropdown -->
|
<div v-else>
|
||||||
|
<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="relative w-40" ref="exportDropdownRef">
|
<div class="relative w-40" ref="exportDropdownRef">
|
||||||
<button @click="isExportOpen = !isExportOpen" 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>
|
||||||
|
<i class="fas fa-spinner fa-spin mr-2"></i> Memproses...
|
||||||
|
</button>
|
||||||
|
<button v-else @click="isExportOpen = !isExportOpen" type="button"
|
||||||
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
|
||||||
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
|
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
@ -59,35 +66,34 @@
|
|||||||
</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>
|
||||||
<tr class="bg-C text-D rounded-t-md">
|
<tr class="bg-C text-D rounded-t-md">
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('nama_produk')"
|
<button @click="handleSort('nama_produk')"
|
||||||
class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors">
|
class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors">
|
||||||
<span>Nama Produk</span>
|
<span>Nama Produk</span>
|
||||||
<i :class="getSortIcon('nama_produk')" class="ml-2"></i>
|
<i :class="getSortIcon('nama_produk')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('jumlah_item_terjual')"
|
<button @click="handleSort('jumlah_item_terjual')"
|
||||||
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
||||||
<span>Item Terjual</span>
|
<span>Item Terjual</span>
|
||||||
<i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i>
|
<i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('berat_terjual')"
|
<button @click="handleSort('berat_terjual')"
|
||||||
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
||||||
<span>Total Berat</span>
|
<span>Total Berat</span>
|
||||||
<i :class="getSortIcon('berat_terjual')" class="ml-2"></i>
|
<i :class="getSortIcon('berat_terjual')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-x border-C px-3 py-3">
|
<th class="border-x border-C px-3 py-3">
|
||||||
<button @click="handleSort('pendapatan')"
|
<button @click="handleSort('pendapatan')"
|
||||||
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
|
||||||
<span>Total Pendapatan</span>
|
<span>Total Pendapatan</span>
|
||||||
<i :class="getSortIcon('pendapatan')" class="ml-2"></i>
|
<i :class="getSortIcon('pendapatan')" class="ml-2"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -113,9 +119,8 @@
|
|||||||
<td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td>
|
<td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td>
|
||||||
<td class="border-x border-C px-3 py-2">
|
<td class="border-x border-C px-3 py-2">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div :ref="el => { if (el) pendapatanElements.push(el) }"
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle"
|
||||||
:style="pendapatanStyle"
|
:class="item.pendapatan == '-' ? 'text-center' : 'text-right'">
|
||||||
:class="item.pendapatan == '-' ? 'text-center' : 'text-right'">
|
|
||||||
{{ item.pendapatan }}
|
{{ item.pendapatan }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -125,19 +130,17 @@
|
|||||||
</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)"
|
<button @click="goToPage(pagination.current_page - 1)" :disabled="pagination.current_page === 1 || loading"
|
||||||
: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">
|
|
||||||
Sebelumnya
|
Sebelumnya
|
||||||
</button>
|
</button>
|
||||||
<span class="text-sm text-D">
|
<span class="text-sm text-D">
|
||||||
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
|
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
|
||||||
</span>
|
</span>
|
||||||
<button @click="goToPage(pagination.current_page + 1)"
|
<button @click="goToPage(pagination.current_page + 1)"
|
||||||
:disabled="(pagination.current_page === pagination.last_page) || loading"
|
:disabled="(pagination.current_page === pagination.last_page) || 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">
|
||||||
Berikutnya
|
Berikutnya
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -157,7 +160,7 @@ const exportDropdownRef = ref(null);
|
|||||||
|
|
||||||
const exportOptions = ref([
|
const exportOptions = ref([
|
||||||
{ value: 'pdf', label: 'Pdf' },
|
{ value: 'pdf', label: 'Pdf' },
|
||||||
{ value: 'xls', label: 'Excel' },
|
{ value: 'xlsx', label: 'Excel' },
|
||||||
{ value: 'csv', label: 'Csv' }
|
{ value: 'csv', label: 'Csv' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -165,6 +168,7 @@ const exportFormat = ref(null);
|
|||||||
const tanggalDipilih = ref('');
|
const tanggalDipilih = ref('');
|
||||||
const data = ref(null);
|
const data = ref(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const loadingExport = ref(false);
|
||||||
|
|
||||||
// Sorting state
|
// Sorting state
|
||||||
const sortBy = ref(null);
|
const sortBy = ref(null);
|
||||||
@ -255,7 +259,7 @@ watch(produk, async (newValue) => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
pendapatanElements.value = [];
|
pendapatanElements.value = [];
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
pendapatanElements.value.forEach(el => {
|
pendapatanElements.value.forEach(el => {
|
||||||
if (el && el.scrollWidth > maxWidth) {
|
if (el && el.scrollWidth > maxWidth) {
|
||||||
@ -269,10 +273,8 @@ watch(produk, async (newValue) => {
|
|||||||
// --- Methods ---
|
// --- Methods ---
|
||||||
const handleSort = (column) => {
|
const handleSort = (column) => {
|
||||||
if (sortBy.value === column) {
|
if (sortBy.value === column) {
|
||||||
// If same column, toggle sort order
|
|
||||||
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
|
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
|
||||||
} else {
|
} else {
|
||||||
// If different column, set new column and default to ascending
|
|
||||||
sortBy.value = column;
|
sortBy.value = column;
|
||||||
sortOrder.value = 'asc';
|
sortOrder.value = 'asc';
|
||||||
}
|
}
|
||||||
@ -282,7 +284,7 @@ const getSortIcon = (column) => {
|
|||||||
if (sortBy.value !== column) {
|
if (sortBy.value !== column) {
|
||||||
return 'fas fa-sort text-D/40'; // Default sort icon
|
return 'fas fa-sort text-D/40'; // Default sort icon
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortOrder.value === 'asc') {
|
if (sortOrder.value === 'asc') {
|
||||||
return 'fas fa-sort-up text-D'; // Ascending
|
return 'fas fa-sort-up text-D'; // Ascending
|
||||||
} else {
|
} else {
|
||||||
@ -299,7 +301,7 @@ const fetchSales = async () => {
|
|||||||
});
|
});
|
||||||
const salesData = response.data;
|
const salesData = response.data;
|
||||||
opsiSales.value = [
|
opsiSales.value = [
|
||||||
{ label: 'Semua Sales', value: null },
|
{ label: 'Semua Sales', value: null },
|
||||||
...salesData.map(sales => ({
|
...salesData.map(sales => ({
|
||||||
label: sales.nama,
|
label: sales.nama,
|
||||||
value: sales.id,
|
value: sales.id,
|
||||||
@ -319,7 +321,8 @@ const fetchNampan = async () => {
|
|||||||
});
|
});
|
||||||
const nampanData = response.data;
|
const nampanData = response.data;
|
||||||
opsiNampan.value = [
|
opsiNampan.value = [
|
||||||
{ label: 'Semua Nampan', value: null },
|
{ label: 'Semua Nampan', value: null },
|
||||||
|
{ label: 'Brankas', value: 0 },
|
||||||
...nampanData.map(nampan => ({
|
...nampanData.map(nampan => ({
|
||||||
label: nampan.nama,
|
label: nampan.nama,
|
||||||
value: nampan.id,
|
value: nampan.id,
|
||||||
@ -337,20 +340,19 @@ const fetchData = async (page = 1) => {
|
|||||||
pendapatanElements.value = [];
|
pendapatanElements.value = [];
|
||||||
|
|
||||||
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`;
|
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`;
|
||||||
if (salesDipilih.value) queryParams += `&sales_id=${salesDipilih.value}`;
|
if (salesDipilih.value != null) queryParams += `&sales_id=${salesDipilih.value}`;
|
||||||
if (nampanDipilih.value) queryParams += `&nampan_id=${nampanDipilih.value}`;
|
if (nampanDipilih.value != null) queryParams += `&nampan_id=${nampanDipilih.value}`;
|
||||||
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
if (namaPembeli.value != null || namaPembeli.value != '') queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/detail-per-produk?${queryParams}`, {
|
const response = await axios.get(`/api/laporan/detail-per-produk?${queryParams}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
@ -358,7 +360,6 @@ 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,
|
||||||
@ -366,7 +367,7 @@ const fetchData = async (page = 1) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gagal mengambil data laporan:', error);
|
console.error('Gagal mengambil data laporan produk:', error);
|
||||||
data.value = null;
|
data.value = null;
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
@ -385,10 +386,42 @@ const goToPage = (page) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectExport = (option) => {
|
const selectExport = async (option) => {
|
||||||
exportFormat.value = option.value;
|
exportFormat.value = option.value;
|
||||||
isExportOpen.value = false;
|
isExportOpen.value = false;
|
||||||
alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`);
|
loadingExport.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/laporan/export/detail-perproduk', {
|
||||||
|
params: {
|
||||||
|
tanggal: tanggalDipilih.value,
|
||||||
|
sales_id: salesDipilih.value,
|
||||||
|
nampan_id: nampanDipilih.value,
|
||||||
|
nama_pembeli: namaPembeli.value,
|
||||||
|
format: exportFormat.value,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Gagal mengekspor laporan per produk:", e);
|
||||||
|
} finally {
|
||||||
|
loadingExport.value = false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDropdownsOnClickOutside = (event) => {
|
const closeDropdownsOnClickOutside = (event) => {
|
||||||
@ -403,8 +436,8 @@ onMounted(() => {
|
|||||||
tanggalDipilih.value = today;
|
tanggalDipilih.value = today;
|
||||||
|
|
||||||
fetchSales();
|
fetchSales();
|
||||||
fetchNampan();
|
fetchNampan(); // Changed from fetchProduk to fetchNampan
|
||||||
|
|
||||||
document.addEventListener('click', closeDropdownsOnClickOutside);
|
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -413,8 +446,8 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Watch for filter changes
|
// Watch for filter changes
|
||||||
watch([tanggalDipilih, salesDipilih, nampanDipilih, namaPembeli], () => {
|
watch([tanggalDipilih, salesDipilih, nampanDipilih, namaPembeli], () => { // Changed from produkDipilih to nampanDipilih
|
||||||
pagination.value.current_page = 1; // Reset to first page when filters change
|
pagination.value.current_page = 1;
|
||||||
fetchData(1);
|
fetchData(1);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
@ -230,12 +230,6 @@ const triggerDownload = async (format) => {
|
|||||||
loadingExport.value = true;
|
loadingExport.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Fucking report with params:', {
|
|
||||||
filter: filterRingkasan.value,
|
|
||||||
format: format,
|
|
||||||
page: pagination.value.current_page,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await axios.get('/api/laporan/export/ringkasan', {
|
const response = await axios.get('/api/laporan/export/ringkasan', {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
171
resources/views/exports/pernampan_pdf.blade.php
Normal file
171
resources/views/exports/pernampan_pdf.blade.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ $title ?? 'Laporan Detail Per Nampan' }}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header h2 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.filter-info {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
.filter-info h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.filter-item {
|
||||||
|
margin: 3px 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.filter-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.rekap-section {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.rekap-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.rekap-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.rekap-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.page-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h2>{{ $title ?? 'Laporan Detail Per Nampan' }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(isset($data['filter']))
|
||||||
|
<div class="filter-info">
|
||||||
|
<h3>Informasi Filter</h3>
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }}
|
||||||
|
</div>
|
||||||
|
@if($data['filter']['nama_sales'])
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Sales:</span> {{ $data['filter']['nama_sales'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($data['filter']['produk'])
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Produk:</span> {{ $data['filter']['produk'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($data['filter']['nama_pembeli'])
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Nama Pembeli:</span> {{ $data['filter']['nama_pembeli'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(isset($data['rekap_harian']))
|
||||||
|
<div class="rekap-section">
|
||||||
|
<div class="rekap-title">Rekap Harian</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Item Terjual:</span> {{ $data['rekap_harian']['total_item_terjual'] }}
|
||||||
|
</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Berat:</span> {{ $data['rekap_harian']['total_berat_terjual'] }}
|
||||||
|
</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Pendapatan:</span> {{ $data['rekap_harian']['total_pendapatan'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40%;">Nama Nampan</th>
|
||||||
|
<th style="width: 20%;" class="text-center">Jumlah Item Terjual</th>
|
||||||
|
<th style="width: 20%;" class="text-right">Berat Terjual</th>
|
||||||
|
<th style="width: 20%;" class="text-right">Pendapatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if(isset($data['nampan']) && count($data['nampan']) > 0)
|
||||||
|
@foreach($data['nampan'] as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $item['nama_nampan'] }}</td>
|
||||||
|
<td class="text-center">{{ $item['jumlah_item_terjual'] }}</td>
|
||||||
|
<td class="text-right">{{ $item['berat_terjual'] }}</td>
|
||||||
|
<td class="text-right">{{ $item['pendapatan'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="no-data">Tidak ada data untuk ditampilkan</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="page-footer">
|
||||||
|
Dicetak pada: {{ \Carbon\Carbon::now()->isoFormat('dddd, D MMMM Y - HH:mm') }} WIB
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
171
resources/views/exports/perproduk_pdf.blade.php
Normal file
171
resources/views/exports/perproduk_pdf.blade.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ $title ?? 'Laporan Detail Per Produk' }}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header h2 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.filter-info {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
.filter-info h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.filter-item {
|
||||||
|
margin: 3px 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.filter-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.rekap-section {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.rekap-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.rekap-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.rekap-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.page-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h2>{{ $title ?? 'Laporan Detail Per Produk' }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(isset($data['filter']))
|
||||||
|
<div class="filter-info">
|
||||||
|
<h3>Informasi Filter</h3>
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }}
|
||||||
|
</div>
|
||||||
|
@if($data['filter']['nama_sales'])
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Sales:</span> {{ $data['filter']['nama_sales'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($data['filter']['nampan'])
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Nampan:</span> {{ $data['filter']['nampan'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($data['filter']['nama_pembeli'])
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Nama Pembeli:</span> {{ $data['filter']['nama_pembeli'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(isset($data['rekap_harian']))
|
||||||
|
<div class="rekap-section">
|
||||||
|
<div class="rekap-title">Rekap Harian</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Item Terjual:</span> {{ $data['rekap_harian']['total_item_terjual'] }}
|
||||||
|
</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Berat:</span> {{ $data['rekap_harian']['total_berat_terjual'] }}
|
||||||
|
</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Pendapatan:</span> {{ $data['rekap_harian']['total_pendapatan'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40%;">Nama Produk</th>
|
||||||
|
<th style="width: 20%;" class="text-center">Jumlah Item Terjual</th>
|
||||||
|
<th style="width: 20%;" class="text-right">Berat Terjual</th>
|
||||||
|
<th style="width: 20%;" class="text-right">Pendapatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if(isset($data['produk']) && count($data['produk']) > 0)
|
||||||
|
@foreach($data['produk'] as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $item['nama_produk'] }}</td>
|
||||||
|
<td class="text-center">{{ $item['jumlah_item_terjual'] }}</td>
|
||||||
|
<td class="text-right">{{ $item['berat_terjual'] }}</td>
|
||||||
|
<td class="text-right">{{ $item['pendapatan'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="no-data">Tidak ada data untuk ditampilkan</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="page-footer">
|
||||||
|
Dicetak pada: {{ \Carbon\Carbon::now()->isoFormat('dddd, D MMMM Y - HH:mm') }} WIB
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -2,40 +2,173 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Laporan Ringkasan</title>
|
<title>{{ $title ?? 'Laporan Ringkasan' }}</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: sans-serif; font-size: 10px; }
|
body {
|
||||||
table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
|
font-family: sans-serif;
|
||||||
th, td { border: 1px solid #ccc; padding: 4px; text-align: left; }
|
font-size: 10px;
|
||||||
th { background-color: #f0f0f0; }
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header h2 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.filter-info {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
.filter-info h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.filter-item {
|
||||||
|
margin: 3px 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.filter-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
.text-right { text-align: right; }
|
.text-right { text-align: right; }
|
||||||
.text-center { text-align: center; }
|
.text-center { text-align: center; }
|
||||||
tr.total-row td { background-color: #f9f9f9; font-weight: bold; }
|
.total-row td {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.rekap-section {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.rekap-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.rekap-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.rekap-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.periode-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.periode-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-bottom: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.page-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2 style="text-align: center;">Laporan Ringkasan {{ ucfirst($filter) }}</h2>
|
<div class="header">
|
||||||
|
<h2>{{ $title ?? 'Laporan Ringkasan ' . ucfirst($filter) }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table>
|
@if(isset($data_filter))
|
||||||
<thead>
|
<div class="filter-info">
|
||||||
<tr>
|
<h3>Informasi Filter</h3>
|
||||||
<th>Tanggal</th>
|
<div class="filter-item">
|
||||||
<th>Nama Sales</th>
|
<span class="filter-label">Periode:</span> {{ ucfirst($filter) }}
|
||||||
<th>Item Terjual</th>
|
</div>
|
||||||
<th>Berat Terjual</th>
|
@if($data_filter['tanggal_mulai'] && $data_filter['tanggal_selesai'])
|
||||||
<th>Pendapatan</th>
|
<div class="filter-item">
|
||||||
</tr>
|
<span class="filter-label">Rentang Tanggal:</span> {{ $data_filter['tanggal_mulai'] }} s/d {{ $data_filter['tanggal_selesai'] }}
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
@endif
|
||||||
@foreach($data as $item)
|
@if($data_filter['sales_filter'])
|
||||||
@php $rowCount = count($item['sales']) > 0 ? count($item['sales']) : 1; @endphp
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">Sales:</span> {{ $data_filter['sales_filter'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(isset($grand_total))
|
||||||
|
<div class="rekap-section">
|
||||||
|
<div class="rekap-title">Rekap {{ ucfirst($filter) }}</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Item Terjual:</span> {{ $grand_total['total_item'] }}
|
||||||
|
</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Berat:</span> {{ $grand_total['total_berat'] }}
|
||||||
|
</div>
|
||||||
|
<div class="rekap-item">
|
||||||
|
<span class="rekap-label">Total Pendapatan:</span> {{ $grand_total['total_pendapatan'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@foreach($data as $item)
|
||||||
|
<div class="periode-section">
|
||||||
|
<div class="periode-header">
|
||||||
|
{{ $item['tanggal'] }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table style="margin-bottom: 0;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 30%;">Nama Sales</th>
|
||||||
|
<th style="width: 20%;" class="text-center">Item Terjual</th>
|
||||||
|
<th style="width: 25%;" class="text-right">Berat Terjual</th>
|
||||||
|
<th style="width: 25%;" class="text-right">Pendapatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
@if(count($item['sales']) > 0)
|
@if(count($item['sales']) > 0)
|
||||||
@foreach($item['sales'] as $index => $sales)
|
@foreach($item['sales'] as $sales)
|
||||||
<tr>
|
<tr>
|
||||||
@if($index == 0)
|
|
||||||
<td rowspan="{{ $rowCount }}">{{ $item['tanggal'] }}</td>
|
|
||||||
@endif
|
|
||||||
<td>{{ $sales['nama'] }}</td>
|
<td>{{ $sales['nama'] }}</td>
|
||||||
<td class="text-center">{{ $sales['item_terjual'] }}</td>
|
<td class="text-center">{{ $sales['item_terjual'] }}</td>
|
||||||
<td class="text-right">{{ $sales['berat_terjual'] }}</td>
|
<td class="text-right">{{ $sales['berat_terjual'] }}</td>
|
||||||
@ -44,20 +177,24 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
@else
|
@else
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $item['tanggal'] }}</td>
|
<td colspan="4" class="no-data">Tidak ada data transaksi</td>
|
||||||
<td colspan="4" class="text-center" style="font-style: italic;">Tidak ada data transaksi</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Baris Total --}}
|
{{-- Baris Total --}}
|
||||||
<tr class="total-row">
|
<tr class="total-row">
|
||||||
<td colspan="2" class="text-right"><strong>Total Periode Ini</strong></td>
|
<td class="text-right"><strong>Total Periode Ini</strong></td>
|
||||||
<td class="text-center"><strong>{{ $item['total_item_terjual'] }}</strong></td>
|
<td class="text-center"><strong>{{ $item['total_item_terjual'] }}</strong></td>
|
||||||
<td class="text-right"><strong>{{ $item['total_berat'] }}</strong></td>
|
<td class="text-right"><strong>{{ $item['total_berat'] }}</strong></td>
|
||||||
<td class="text-right"><strong>{{ $item['total_pendapatan'] }}</strong></td>
|
<td class="text-right"><strong>{{ $item['total_pendapatan'] }}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
<div class="page-footer">
|
||||||
|
Dicetak pada: {{ \Carbon\Carbon::now()->isoFormat('dddd, D MMMM Y - HH:mm') }} WIB
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -45,8 +45,8 @@ Route::prefix('api')->group(function () {
|
|||||||
Route::get('detail-per-nampan', [LaporanController::class, 'detailPerNampan']);
|
Route::get('detail-per-nampan', [LaporanController::class, 'detailPerNampan']);
|
||||||
|
|
||||||
Route::get('export/ringkasan', [LaporanController::class, 'exportRingkasan']);
|
Route::get('export/ringkasan', [LaporanController::class, 'exportRingkasan']);
|
||||||
Route::get('export/detail-produk', [LaporanController::class, 'exportDetailProduk']);
|
Route::get('export/detail-pernampan', [LaporanController::class, 'exportDetailNampan']);
|
||||||
Route::get('export/detail-nampan', [LaporanController::class, 'exportDetailNampan']);
|
Route::get('export/detail-perproduk', [LaporanController::class, 'exportDetailProduk']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user