Kasir/app/Services/LaporanService.php
2025-09-10 13:32:58 +07:00

423 lines
16 KiB
PHP

<?php
namespace App\Services;
use App\Models\ItemTransaksi;
use App\Models\Produk;
use App\Models\Transaksi;
use App\Models\Sales;
use App\Models\Nampan;
use App\Repositories\TransaksiRepository;
use App\Helpers\LaporanHelper;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Maatwebsite\Excel\Facades\Excel;
use Barryvdh\DomPDF\Facade\Pdf;
use App\Exports\RingkasanExport;
use App\Exports\DetailProdukExport;
use App\Exports\DetailNampanExport;
class LaporanService
{
private const CACHE_TTL = 300; // 5 menit
private const DEFAULT_PER_PAGE = 15;
private const MAX_PER_PAGE = 100;
private const DAILY_PER_PAGE = 7;
private const MONTHLY_PER_PAGE = 12;
private const PAGINATION_DAYS_LIMIT = 365;
private TransaksiRepository $transaksiRepo;
private LaporanHelper $helper;
public function __construct(TransaksiRepository $transaksiRepo, LaporanHelper $helper)
{
$this->transaksiRepo = $transaksiRepo;
$this->helper = $helper;
}
public function getRingkasan(string $filter, int $page)
{
$cacheKey = "laporan_ringkasan_{$filter}_page_{$page}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filter, $page) {
$allSalesNames = $this->getAllSalesNames();
if ($filter === 'hari') {
return $this->processLaporanHarian($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)
{
$tanggal = Carbon::parse($params['tanggal']);
$page = $params['page'] ?? 1;
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
// --- Step 1: Calculate overall totals for all filtered items ---
// We need a separate query for totals that is not affected by pagination.
$totalsQuery = $this->buildBaseItemQuery($tanggal);
$this->applyFilters($totalsQuery, $params);
$totalsResult = $totalsQuery->select(
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();
$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(
'produks.id as id_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');
// Apply filters to the subquery
$this->applyFilters($salesSubQuery, $params);
// --- Step 3: Fetch paginated products and LEFT JOIN the sales data subquery ---
$semuaProdukPaginated = Produk::select(
'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);
// --- 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);
return [
'filter' => $filterInfo,
'rekap_harian' => $rekapHarian,
'produk' => $detailItem,
'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated),
];
}
public function getDetailPerNampan(array $params)
{
$tanggal = Carbon::parse($params['tanggal']);
$page = $params['page'] ?? 1;
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
$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);
$semuaNampanPaginated = $this->helper->getAllNampanWithPagination($page, $perPage);
$detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual);
$filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params);
return [
'filter' => $filterInfo,
'rekap_harian' => $totals,
'nampan' => $detailItem->values(),
'pagination' => $this->helper->buildPaginationInfo($semuaNampanPaginated),
];
}
public function exportRingkasan(array $params)
{
$filter = $params['filter'];
$format = $params['format'];
$page = $params['page'] ?? 1;
$allSalesNames = $this->getAllSalesNames();
if ($filter === 'hari') {
// Tar kalau mau ubah eksport laporan setiap hari, param ke-3 jadiin false #Bagas
$data = $this->processLaporanHarian($allSalesNames, $page, true);
} else {
$data = $this->processLaporanBulanan($allSalesNames, $page, true);
}
$fileName = "laporan_ringkasan_{$filter}_" . Carbon::now()->format('Ymd') . ".{$format}";
if ($format === 'pdf') {
$viewData = method_exists($data, 'items') ? $data->items() : $data;
$pdf = PDF::loadView('exports.ringkasan_pdf', [
'data' => $viewData,
'filter' => $filter
]);
$pdf->setPaper('a4', 'potrait');
return $pdf->download($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
{
return Cache::remember('all_sales_names', self::CACHE_TTL, function () {
return Transaksi::select('nama_sales')->distinct()->pluck('nama_sales');
});
}
private function processLaporanHarian(Collection $allSalesNames, int $page = 1, bool $limitPagination = true)
{
return $this->transaksiRepo->processLaporanHarian($allSalesNames, $page, $limitPagination);
}
private function processLaporanBulanan(Collection $allSalesNames, int $page = 1, bool $limitPagination = true)
{
return $this->transaksiRepo->processLaporanBulanan($allSalesNames, $page, $limitPagination);
}
private function buildBaseItemQuery(Carbon $carbonDate)
{
return ItemTransaksi::query()
->join('items', 'item_transaksis.id_item', '=', 'items.id')
->join('produks', 'items.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'])) {
$query->join('sales', 'transaksis.id_sales', '=', 'sales.id')
->where('sales.id', $params['sales_id']);
}
if (isset($params['nampan_id'])) {
if ($params['nampan_id'] == 0) {
$query->whereNull('items.id_nampan');
} else {
$query->where('items.id_nampan', $params['nampan_id']);
}
}
if (!empty($params['nama_pembeli'])) {
$query->where('transaksis.nama_pembeli', 'like', "%{$params['nama_pembeli']}%");
}
}
private function applyNampanFilters($query, array $params): void
{
if (!empty($params['sales_id'])) {
$query->join('sales', 'transaksis.id_sales', '=', 'sales.id')
->where('sales.id', $params['sales_id']);
}
if (!empty($params['produk_id'])) {
$query->where('produks.id', $params['produk_id']);
}
if (!empty($params['nama_pembeli'])) {
$query->where('transaksis.nama_pembeli', 'like', "%{$params['nama_pembeli']}%");
}
}
}