479 lines
18 KiB
PHP
479 lines
18 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 sales detail aggregated by product (NO PAGINATION - all data).
|
|
*
|
|
* @param array $params Filter parameters (tanggal, sales_id, nampan_id, nama_pembeli)
|
|
* @return array Report data structure
|
|
* @throws \Exception
|
|
*/
|
|
public function getDetailPerProduk(array $params)
|
|
{
|
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
|
|
|
// TAMBAH: Validasi range max 30 hari
|
|
if ($startDate->diffInDays($endDate) > 30) {
|
|
throw new \InvalidArgumentException('Interval tanggal maksimal 30 hari.');
|
|
}
|
|
|
|
// FIXED: Skip pagination params untuk data utama
|
|
$page = $params['page'] ?? 1;
|
|
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
|
|
|
// --- Step 1: Totals ---
|
|
$totalsQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
|
$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();
|
|
|
|
$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: Subquery for all products ---
|
|
$salesSubQuery = $this->buildBaseItemQueryForRange($startDate, $endDate)
|
|
->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');
|
|
|
|
$this->applyFilters($salesSubQuery, $params);
|
|
|
|
// --- Step 3: All products (NO PAGINATION) ---
|
|
$semuaProduk = 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')
|
|
->get(); // FIXED: get() instead of paginate()
|
|
|
|
// --- Step 4: Map & filter ---
|
|
$detailItem = $semuaProduk->map(function ($item) {
|
|
return [
|
|
'nama_produk' => $item->nama_produk,
|
|
'jumlah_item_terjual' => $item->jumlah_item_terjual ? (int) $item->jumlah_item_terjual : 0,
|
|
'berat_terjual' => $item->berat_terjual ? $this->helper->formatWeight($item->berat_terjual) : '-',
|
|
'pendapatan' => $item->pendapatan ? $this->helper->formatCurrency($item->pendapatan) : '-',
|
|
];
|
|
})->filter(function ($item) {
|
|
return $item['jumlah_item_terjual'] > 0;
|
|
});
|
|
|
|
// FIXED: Simple collection without pagination
|
|
$filteredCollection = $detailItem->values();
|
|
|
|
// --- Step 5: Response ---
|
|
$filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params);
|
|
|
|
return [
|
|
'filter' => $filterInfo,
|
|
'rekap_interval' => $rekapInterval,
|
|
'produk' => $filteredCollection,
|
|
'pagination' => [
|
|
'current_page' => 1,
|
|
'from' => 1,
|
|
'last_page' => 1,
|
|
'per_page' => $filteredCollection->count(),
|
|
'to' => $filteredCollection->count(),
|
|
'total' => $filteredCollection->count(),
|
|
'has_more_pages' => false,
|
|
],
|
|
];
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
/**
|
|
* Get sales detail aggregated by nampan (NO PAGINATION - all data).
|
|
*
|
|
* @param array $params Filter parameters (tanggal, sales_id, produk_id, nama_pembeli)
|
|
* @return array Report data structure
|
|
* @throws \Exception
|
|
*/
|
|
public function getDetailPerNampan(array $params)
|
|
{
|
|
$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->buildBaseItemQueryForRange($startDate, $endDate);
|
|
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
|
|
|
$nampanTerjual = $nampanTerjualQuery
|
|
->select(
|
|
DB::raw('COALESCE(item_transaksis.posisi_asal, "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('nama_nampan')
|
|
->get()
|
|
->keyBy('nama_nampan');
|
|
|
|
// 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);
|
|
|
|
// FIXED: Get all nampan without pagination
|
|
$semuaNampan = $this->helper->getAllNampanWithPagination(1, PHP_INT_MAX); // Skip pagination
|
|
$detailItem = $this->helper->mapNampanWithSalesData($semuaNampan, $nampanTerjual)
|
|
->filter(function ($item) {
|
|
return $item['jumlah_item_terjual'] > 0;
|
|
});
|
|
|
|
// FIXED: Simple collection without pagination
|
|
$filteredCollection = $detailItem->values();
|
|
|
|
$filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params);
|
|
|
|
return [
|
|
'filter' => $filterInfo,
|
|
'rekap_interval' => $totals,
|
|
'nampan' => $filteredCollection,
|
|
'pagination' => [
|
|
'current_page' => 1,
|
|
'from' => 1,
|
|
'last_page' => 1,
|
|
'per_page' => $filteredCollection->count(),
|
|
'to' => $filteredCollection->count(),
|
|
'total' => $filteredCollection->count(),
|
|
'has_more_pages' => false,
|
|
],
|
|
];
|
|
}
|
|
|
|
public function exportRingkasan(array $params)
|
|
{
|
|
$filter = $params['filter'];
|
|
$format = $params['format'];
|
|
$page = $params['page'] ?? 1;
|
|
|
|
$allSalesNames = $this->getAllSalesNames();
|
|
|
|
if ($filter === 'hari') {
|
|
$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);
|
|
}
|
|
|
|
public function exportPerProduk(array $params)
|
|
{
|
|
$format = $params['format'];
|
|
$allParams = $params;
|
|
unset($allParams['page'], $allParams['per_page']);
|
|
|
|
$data = $this->getDetailPerProdukForExport($allParams);
|
|
|
|
$startDate = Carbon::parse($params['start_date'])->format('Ymd');
|
|
$endDate = Carbon::parse($params['end_date'])->format('Ymd');
|
|
$fileName = "laporan_per_produk_{$startDate}_to_{$endDate}.{$format}";
|
|
|
|
if ($format === 'pdf') {
|
|
$pdf = PDF::loadView('exports.perproduk_pdf', [
|
|
'data' => $data,
|
|
'title' => 'Laporan Detail Per Produk'
|
|
]);
|
|
$pdf->setPaper('a4', 'portrait');
|
|
return $pdf->download($fileName);
|
|
}
|
|
|
|
return Excel::download(new DetailProdukExport($data), $fileName);
|
|
}
|
|
|
|
public function exportPerNampan(array $params)
|
|
{
|
|
$format = $params['format'];
|
|
$allParams = $params;
|
|
unset($allParams['page'], $allParams['per_page']);
|
|
|
|
$data = $this->getDetailPerNampanForExport($allParams);
|
|
|
|
$startDate = Carbon::parse($params['start_date'])->format('Ymd');
|
|
$endDate = Carbon::parse($params['end_date'])->format('Ymd');
|
|
$fileName = "laporan_per_nampan_{$startDate}_to_{$endDate}.{$format}";
|
|
|
|
if ($format === 'pdf') {
|
|
$pdf = PDF::loadView('exports.pernampan_pdf', [
|
|
'data' => $data,
|
|
'title' => 'Laporan Detail Per Nampan'
|
|
]);
|
|
$pdf->setPaper('a4', 'portrait');
|
|
return $pdf->download($fileName);
|
|
}
|
|
|
|
return Excel::download(new DetailNampanExport($data), $fileName);
|
|
}
|
|
|
|
private function getDetailPerProdukForExport(array $params)
|
|
{
|
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
|
|
|
$produkTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
|
$this->applyFilters($produkTerjualQuery, $params);
|
|
|
|
$produkTerjual = $produkTerjualQuery
|
|
->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');
|
|
|
|
// 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();
|
|
|
|
$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 null;
|
|
})->filter();
|
|
|
|
$filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params);
|
|
|
|
return [
|
|
'filter' => $filterInfo,
|
|
'rekap_interval' => $totals,
|
|
'produk' => $detailItem->values(),
|
|
];
|
|
}
|
|
|
|
private function getDetailPerNampanForExport(array $params)
|
|
{
|
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
|
|
|
$nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
|
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
|
|
|
$nampanTerjual = $nampanTerjualQuery
|
|
->select(
|
|
'item_transaksis.posisi_asal as posisi_asal',
|
|
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('item_transaksis.posisi_asal')
|
|
->get()
|
|
->keyBy('posisi_asal');
|
|
|
|
// 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])
|
|
->select('posisi_asal')
|
|
->distinct()
|
|
->pluck('posisi_asal')
|
|
->sort()
|
|
->values();
|
|
|
|
$detailItem = $semuaPosisi->map(function ($posisi) use ($nampanTerjual) {
|
|
if ($nampanTerjual->has($posisi)) {
|
|
$dataTerjual = $nampanTerjual->get($posisi);
|
|
return [
|
|
'nama_nampan' => $posisi,
|
|
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual,
|
|
'berat_terjual' => $this->helper->formatWeight($dataTerjual->berat_terjual),
|
|
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
|
];
|
|
}
|
|
return null;
|
|
})->filter();
|
|
|
|
$filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params);
|
|
|
|
return [
|
|
'filter' => $filterInfo,
|
|
'rekap_interval' => $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 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'])) {
|
|
$nampanId = (int) $params['nampan_id'];
|
|
if ($nampanId === -1) {
|
|
$query->where('item_transaksis.posisi_asal', 'Brankas');
|
|
} elseif ($nampanId > 0) {
|
|
$query->join('nampans', function ($join) use ($nampanId) {
|
|
$join->on('item_transaksis.posisi_asal', '=', 'nampans.nama')
|
|
->where('nampans.id', $nampanId);
|
|
});
|
|
}
|
|
}
|
|
|
|
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']}%");
|
|
}
|
|
}
|
|
} |