MIF_E31231972/app/Http/Controllers/ReportController.php

532 lines
21 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\StockMovement;
use Carbon\Carbon;
use Dompdf\Dompdf;
use Illuminate\Http\Request;
use Illuminate\View\View;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Writer\Xls;
class ReportController extends Controller
{
public function pembelian(Request $request): View
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with(['product', 'user'])
->kategori(StockMovement::KATEGORI_MASUK)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->latest(StockMovement::columnDibuat())
->get();
return view('reports.pembelian', [
'tanggalMulai' => $tanggalMulai,
'tanggalSelesai' => $tanggalSelesai,
'data' => $data,
'total' => (int) $data->sum('jumlah'),
]);
}
public function penjualan(Request $request): View
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with(['product', 'user'])
->kategori(StockMovement::KATEGORI_KELUAR)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->latest(StockMovement::columnDibuat())
->get();
return view('reports.penjualan', [
'tanggalMulai' => $tanggalMulai,
'tanggalSelesai' => $tanggalSelesai,
'data' => $data,
'total' => (int) $data->sum('jumlah'),
]);
}
public function rekapPembelian(Request $request): View
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with('product')
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
->kategori(StockMovement::KATEGORI_MASUK)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->groupBy('id_barang')
->orderByDesc('total_jumlah')
->get();
return view('reports.rekap-pembelian', [
'tanggalMulai' => $tanggalMulai,
'tanggalSelesai' => $tanggalSelesai,
'data' => $data,
'total' => (int) $data->sum('total_jumlah'),
]);
}
public function rekapPenjualan(Request $request): View
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with('product')
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
->kategori(StockMovement::KATEGORI_KELUAR)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->groupBy('id_barang')
->orderByDesc('total_jumlah')
->get();
return view('reports.rekap-penjualan', [
'tanggalMulai' => $tanggalMulai,
'tanggalSelesai' => $tanggalSelesai,
'data' => $data,
'total' => (int) $data->sum('total_jumlah'),
]);
}
public function barangHilang(Request $request): View
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with(['product', 'user'])
->kategori(StockMovement::KATEGORI_HILANG)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->latest(StockMovement::columnDibuat())
->get();
return view('reports.barang-hilang', [
'tanggalMulai' => $tanggalMulai,
'tanggalSelesai' => $tanggalSelesai,
'data' => $data,
'total' => (int) $data->sum('jumlah'),
]);
}
public function exportPembelian(Request $request)
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with(['product', 'user'])
->kategori(StockMovement::KATEGORI_MASUK)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->latest(StockMovement::columnDibuat())
->get();
return $this->exportReport(
$request,
'Laporan Pembelian',
$data,
['Tanggal', 'Produk', 'Jumlah', 'User', 'Keterangan'],
fn ($item) => [
$item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '-',
$item->product->nama ?? '-',
$item->jumlah,
$item->user->name ?? '-',
$item->keterangan ?? '-',
],
'laporan-pembelian-' . now()->format('YmdHis')
);
}
public function exportPenjualan(Request $request)
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with(['product', 'user'])
->kategori(StockMovement::KATEGORI_KELUAR)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->latest(StockMovement::columnDibuat())
->get();
return $this->exportReport(
$request,
'Laporan Penjualan',
$data,
['Tanggal', 'Produk', 'Jumlah', 'User', 'Keterangan'],
fn ($item) => [
$item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '-',
$item->product->nama ?? '-',
$item->jumlah,
$item->user->name ?? '-',
$item->keterangan ?? '-',
],
'laporan-penjualan-' . now()->format('YmdHis')
);
}
public function exportRekapPembelian(Request $request)
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with('product')
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
->kategori(StockMovement::KATEGORI_MASUK)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->groupBy('id_barang')
->orderByDesc('total_jumlah')
->get();
return $this->exportReport(
$request,
'Rekap Pembelian',
$data,
['Produk', 'Total Qty', 'Total Transaksi'],
fn ($item) => [
$item->product->nama ?? '-',
(int) $item->total_jumlah,
(int) $item->total_transaksi,
],
'rekap-pembelian-' . now()->format('YmdHis')
);
}
public function exportRekapPenjualan(Request $request)
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with('product')
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
->kategori(StockMovement::KATEGORI_KELUAR)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->groupBy('id_barang')
->orderByDesc('total_jumlah')
->get();
return $this->exportReport(
$request,
'Rekap Penjualan',
$data,
['Produk', 'Total Qty', 'Total Transaksi'],
fn ($item) => [
$item->product->nama ?? '-',
(int) $item->total_jumlah,
(int) $item->total_transaksi,
],
'rekap-penjualan-' . now()->format('YmdHis')
);
}
public function exportBarangHilang(Request $request)
{
$this->ensureReportAccess();
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
$data = StockMovement::with(['product', 'user'])
->kategori(StockMovement::KATEGORI_HILANG)
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
->latest(StockMovement::columnDibuat())
->get();
return $this->exportReport(
$request,
'Laporan Barang Hilang',
$data,
['Tanggal', 'Produk', 'Jumlah', 'User', 'Keterangan'],
fn ($item) => [
$item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '-',
$item->product->nama ?? '-',
$item->jumlah,
$item->user->name ?? '-',
$item->keterangan ?? '-',
],
'barang-hilang-' . now()->format('YmdHis')
);
}
private function exportReport(Request $request, string $title, iterable $data, array $columns, callable $rowMapper, string $filenameBase)
{
$rows = collect($data)->map($rowMapper)->toArray();
if ($request->input('format') === 'pdf') {
return $this->exportPdf($title, $filenameBase . '.pdf', $columns, $rows, [
'tanggalMulai' => $request->input('tanggal_mulai'),
'tanggalSelesai' => $request->input('tanggal_selesai'),
]);
}
return $this->exportExcel($title, $filenameBase . '.xls', $columns, $rows);
}
private function exportExcel(string $title, string $filename, array $columns, array $rows)
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Laporan');
$lastColumn = Coordinate::stringFromColumnIndex(max(1, count($columns)));
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', $title);
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
$sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
foreach ($columns as $index => $column) {
$sheet->setCellValueByColumnAndRow($index + 1, 3, $column);
}
if ($rows === []) {
$sheet->mergeCells("A4:{$lastColumn}4");
$sheet->setCellValue('A4', 'Tidak ada data.');
} else {
foreach ($rows as $rowIndex => $row) {
foreach ($columns as $columnIndex => $column) {
$sheet->setCellValueByColumnAndRow(
$columnIndex + 1,
$rowIndex + 4,
$row[$columnIndex] ?? ''
);
}
}
}
$lastRow = max(4, count($rows) + 3);
$tableRange = "A3:{$lastColumn}{$lastRow}";
$sheet->getStyle("A3:{$lastColumn}3")->applyFromArray([
'font' => ['bold' => true],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['rgb' => 'E8EEF6'],
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER,
],
]);
$sheet->getStyle($tableRange)->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['rgb' => 'B9C0C9'],
],
],
'alignment' => [
'vertical' => Alignment::VERTICAL_TOP,
'wrapText' => true,
],
]);
foreach (range(1, count($columns)) as $columnIndex) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex))->setAutoSize(true);
}
$sheet->freezePane('A4');
ob_start();
(new Xls($spreadsheet))->save('php://output');
$content = ob_get_clean();
$spreadsheet->disconnectWorksheets();
return response($content, 200, [
'Content-Type' => 'application/vnd.ms-excel',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Cache-Control' => 'max-age=0',
]);
}
private function exportPdf(string $title, string $filename, array $columns, array $rows, array $filters)
{
$period = ($filters['tanggalMulai'] || $filters['tanggalSelesai'])
? ($filters['tanggalMulai'] ?: '-') . ' s/d ' . ($filters['tanggalSelesai'] ?: '-')
: 'Semua tanggal';
$html = view('reports.exports.table', [
'title' => $title,
'period' => $period,
'tanggalMulai' => $filters['tanggalMulai'],
'tanggalSelesai' => $filters['tanggalSelesai'],
'printedAt' => now('Asia/Jakarta')->format('d/m/Y H:i'),
'columns' => $columns,
'rows' => $rows,
])->render();
try {
$pdf = new Dompdf();
$pdf->set_option('defaultFont', 'Arial');
$pdf->set_option('isHtml5ParserEnabled', true);
$pdf->set_option('isRemoteEnabled', true);
$pdf->loadHtml($html);
$pdf->setPaper('A4', 'landscape');
$pdf->render();
$output = $pdf->output();
} catch (\Throwable $exception) {
report($exception);
$output = $this->buildFallbackPdf($title, $period, $columns, $rows);
}
return response($output, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}
private function buildFallbackPdf(string $title, string $period, array $columns, array $rows): string
{
if ($rows === []) {
$rows = [['Tidak ada data.']];
}
$pageWidth = 842;
$pageHeight = 595;
$margin = 45;
$tableTop = 470;
$rowHeight = 28;
$headerHeight = 30;
$rowsPerPage = 14;
$columnWidths = match (count($columns)) {
3 => [360, 170, 222],
5 => [135, 180, 75, 110, 252],
default => array_fill(0, max(1, count($columns)), floor(($pageWidth - ($margin * 2)) / max(1, count($columns)))),
};
$pages = array_chunk($rows, $rowsPerPage);
$objects = [];
$pageObjectIds = [];
$nextObjectId = 5;
foreach ($pages as $pageIndex => $pageRows) {
$content = '';
$content .= $this->fallbackPdfText(strtoupper($title), $margin, 545, 15, true);
$content .= $this->fallbackPdfText('Periode: ' . $period, $margin, 524, 10);
$content .= $this->fallbackPdfText('Dicetak: ' . now('Asia/Jakarta')->format('d/m/Y H:i'), $margin, 508, 9);
$x = $margin;
foreach ($columns as $index => $column) {
$width = $columnWidths[$index] ?? end($columnWidths);
$content .= $this->fallbackPdfCell($x, $tableTop, $width, $headerHeight, (string) $column, true);
$x += $width;
}
$y = $tableTop - $rowHeight;
foreach ($pageRows as $row) {
$x = $margin;
foreach ($columns as $index => $column) {
$width = $columnWidths[$index] ?? end($columnWidths);
$content .= $this->fallbackPdfCell($x, $y, $width, $rowHeight, (string) ($row[$index] ?? ''));
$x += $width;
}
$y -= $rowHeight;
}
$content .= $this->fallbackPdfText('Halaman ' . ($pageIndex + 1) . ' dari ' . count($pages), $pageWidth - 125, 32, 8);
$contentObjectId = $nextObjectId++;
$objects[$contentObjectId] = "<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream";
$pageObjectId = $nextObjectId++;
$pageObjectIds[] = $pageObjectId;
$objects[$pageObjectId] = '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' . $pageWidth . ' ' . $pageHeight . '] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents ' . $contentObjectId . ' 0 R >>';
}
$kids = implode(' ', array_map(fn ($id) => $id . ' 0 R', $pageObjectIds));
$objects = [
1 => '<< /Type /Catalog /Pages 2 0 R >>',
2 => '<< /Type /Pages /Kids [' . $kids . '] /Count ' . count($pageObjectIds) . ' >>',
3 => '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
4 => '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>',
] + $objects;
ksort($objects);
$pdf = "%PDF-1.4\n";
$offsets = [0];
foreach ($objects as $id => $object) {
$offsets[$id] = strlen($pdf);
$pdf .= $id . " 0 obj\n" . $object . "\nendobj\n";
}
$xrefOffset = strlen($pdf);
$pdf .= "xref\n0 " . (count($objects) + 1) . "\n0000000000 65535 f \n";
for ($i = 1; $i <= count($objects); $i++) {
$pdf .= sprintf("%010d 00000 n \n", $offsets[$i]);
}
return $pdf . "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n" . $xrefOffset . "\n%%EOF";
}
private function fallbackPdfCell(float $x, float $y, float $width, float $height, string $text, bool $header = false): string
{
$fontSize = $header ? 9 : 8.5;
$maxChars = max(6, (int) floor(($width - 12) / ($fontSize * 0.55)));
$text = strlen($text) > $maxChars ? substr($text, 0, $maxChars - 3) . '...' : $text;
$content = $header ? "0.90 0.94 0.98 rg\n{$x} {$y} {$width} {$height} re f\n" : '';
$content .= "0.25 0.30 0.36 RG\n0.6 w\n{$x} {$y} {$width} {$height} re S\n";
$content .= $this->fallbackPdfText($text, $x + 6, $y + (($height - $fontSize) / 2), $fontSize, $header);
return $content;
}
private function fallbackPdfText(string $text, float $x, float $y, float $fontSize = 10, bool $bold = false): string
{
$font = $bold ? 'F2' : 'F1';
$text = str_replace(['\\', '(', ')'], ['\\\\', '\(', '\)'], $text);
return "0 0 0 rg\nBT\n/{$font} {$fontSize} Tf\n{$x} {$y} Td\n({$text}) Tj\nET\n";
}
private function resolveTanggal(Request $request): array
{
$tanggalMulai = $request->input('tanggal_mulai');
$tanggalSelesai = $request->input('tanggal_selesai');
if ($tanggalMulai && !$tanggalSelesai) {
$tanggalSelesai = $tanggalMulai;
}
return [$tanggalMulai, $tanggalSelesai];
}
/**
* Batas tanggal cocok dengan jam di web (hari penuh WIB), tanpa menyimpan sebagai UTC lain.
*/
private function mulaiHariDiJakarta(string $tanggal): Carbon
{
return Carbon::parse($tanggal, 'Asia/Jakarta')->startOfDay();
}
private function akhirHariDiJakarta(string $tanggal): Carbon
{
return Carbon::parse($tanggal, 'Asia/Jakarta')->endOfDay();
}
private function kolomWaktuMutasi(): string
{
return StockMovement::columnDibuat();
}
private function ensureReportAccess(): void
{
abort_unless(in_array(auth()->user()?->role, ['admin', 'owner'], true), 403);
}
}