SimStok_Mebel/app/Http/Controllers/DashboardController.php

238 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\StockMovement;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class DashboardController extends Controller
{
public function index(Request $request): View
{
$totalProduk = Product::count();
$daftarNamaProduk = Product::orderBy('nama')->pluck('nama');
$stokMenipis = Product::whereColumn('stok', '<=', 'stok_minimum')->count();
$jumlahBarangMasuk = (int) StockMovement::kategori(StockMovement::KATEGORI_MASUK)->sum('jumlah');
$jumlahBarangKeluar = (int) StockMovement::kategori(StockMovement::KATEGORI_KELUAR)->sum('jumlah');
$tz = 'Asia/Jakarta';
$kolomWaktuMutasi = StockMovement::columnDibuat();
$awalBulanIni = Carbon::now($tz)->startOfMonth();
$akhirBulanIni = Carbon::now($tz)->endOfMonth();
$awalBulanLalu = Carbon::now($tz)->subMonthNoOverflow()->startOfMonth();
$akhirBulanLalu = Carbon::now($tz)->subMonthNoOverflow()->endOfMonth();
$jumlahBarangMasukBulanIni = (int) StockMovement::kategori(StockMovement::KATEGORI_MASUK)
->whereBetween($kolomWaktuMutasi, [$awalBulanIni, $akhirBulanIni])
->sum('jumlah');
$jumlahBarangMasukBulanLalu = (int) StockMovement::kategori(StockMovement::KATEGORI_MASUK)
->whereBetween($kolomWaktuMutasi, [$awalBulanLalu, $akhirBulanLalu])
->sum('jumlah');
$jumlahBarangKeluarBulanIni = (int) StockMovement::kategori(StockMovement::KATEGORI_KELUAR)
->whereBetween($kolomWaktuMutasi, [$awalBulanIni, $akhirBulanIni])
->sum('jumlah');
$jumlahBarangKeluarBulanLalu = (int) StockMovement::kategori(StockMovement::KATEGORI_KELUAR)
->whereBetween($kolomWaktuMutasi, [$awalBulanLalu, $akhirBulanLalu])
->sum('jumlah');
$perubahanBarangMasuk = $jumlahBarangMasukBulanIni - $jumlahBarangMasukBulanLalu;
$perubahanBarangKeluar = $jumlahBarangKeluarBulanIni - $jumlahBarangKeluarBulanLalu;
$labelBulanPerbandinganPenjualan = $awalBulanIni->copy()->translatedFormat('F Y');
$produkStokMenipis = Product::whereColumn('stok', '<=', 'stok_minimum')
->orderBy('stok')
->get();
$aktivitasTerbaru = StockMovement::with('product')
->latest(StockMovement::columnDibuat())
->limit(6)
->get();
$pesanWhatsAppStokMinimum = "Peringatan Stok Minimum\n\n";
if ($produkStokMenipis->isNotEmpty()) {
$pesanWhatsAppStokMinimum .= "Barang berikut perlu segera direstock:\n";
foreach ($produkStokMenipis as $produk) {
$pesanWhatsAppStokMinimum .= "- {$produk->nama}: stok {$produk->stok}, minimum {$produk->stok_minimum}\n";
}
$pesanWhatsAppStokMinimum .= "\nMohon segera dilakukan pengadaan barang.";
} else {
$pesanWhatsAppStokMinimum .= 'Semua stok barang masih aman.';
}
$nomorPenerimaWhatsApp = collect(config('services.whatsapp.target_numbers', []));
$stokAman = max(0, $totalProduk - $stokMenipis);
$persentaseStokAman = $totalProduk > 0 ? (int) round(($stokAman / $totalProduk) * 100) : 100;
$topPergerakanProduk = StockMovement::with('product')
->selectRaw('id_barang, SUM(jumlah) as total')
->groupBy('id_barang')
->orderByDesc('total')
->limit(5)
->get();
$perbandinganProdukTerjual = StockMovement::with('product')
->selectRaw('id_barang, SUM(jumlah) as total_terjual')
->kategori(StockMovement::KATEGORI_KELUAR)
->whereBetween($kolomWaktuMutasi, [$awalBulanIni, $akhirBulanIni])
->groupBy('id_barang')
->havingRaw('SUM(jumlah) > 0')
->orderByDesc('total_terjual')
->get();
$produkTercepatTerjual = $perbandinganProdukTerjual->first();
$produkTerlambatTerjual = $perbandinganProdukTerjual->sortBy('total_terjual')->first();
$selisihProdukTerjual = ($produkTercepatTerjual && $produkTerlambatTerjual)
? (int) $produkTercepatTerjual->total_terjual - (int) $produkTerlambatTerjual->total_terjual
: 0;
$batasSelisihProdukMirip = 5;
$produkCepatTerjualLainnya = collect();
$produkLambatTerjualLainnya = collect();
if ($produkTercepatTerjual) {
$totalTercepat = (int) $produkTercepatTerjual->total_terjual;
$produkCepatTerjualLainnya = $perbandinganProdukTerjual
->filter(function ($item) use ($produkTercepatTerjual, $totalTercepat, $batasSelisihProdukMirip): bool {
return $item->id_barang !== $produkTercepatTerjual->id_barang
&& ((int) $item->total_terjual >= $totalTercepat - $batasSelisihProdukMirip);
})
->take(4)
->values();
}
if ($produkTerlambatTerjual) {
$totalTerlambat = (int) $produkTerlambatTerjual->total_terjual;
$produkLambatTerjualLainnya = $perbandinganProdukTerjual
->sortBy('total_terjual')
->filter(function ($item) use ($produkTerlambatTerjual, $totalTerlambat, $batasSelisihProdukMirip): bool {
return $item->id_barang !== $produkTerlambatTerjual->id_barang
&& ((int) $item->total_terjual <= $totalTerlambat + $batasSelisihProdukMirip);
})
->take(4)
->values();
}
$tahunDipilih = (int) $request->input('tahun', Carbon::now('Asia/Jakarta')->year);
$jenisData = $request->input('jenis_data', 'semua');
if (!in_array($jenisData, ['semua', 'masuk', 'keluar'], true)) {
$jenisData = 'semua';
}
$dataPertama = StockMovement::oldest($kolomWaktuMutasi)->value($kolomWaktuMutasi);
$dataTerakhir = StockMovement::latest($kolomWaktuMutasi)->value($kolomWaktuMutasi);
$tahunAwal = $dataPertama ? Carbon::parse($dataPertama)->timezone('Asia/Jakarta')->year : $tahunDipilih;
$tahunAkhir = $dataTerakhir ? Carbon::parse($dataTerakhir)->timezone('Asia/Jakarta')->year : $tahunDipilih;
$daftarTahun = collect(range($tahunAkhir, $tahunAwal));
if ($daftarTahun->isEmpty()) {
$daftarTahun = collect([$tahunDipilih]);
} elseif (!$daftarTahun->contains($tahunDipilih)) {
$daftarTahun = $daftarTahun->push($tahunDipilih)->sortDesc()->values();
}
$labelBulan = [1 => 'Jan', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mei', 6 => 'Jun', 7 => 'Jul', 8 => 'Agu', 9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Des'];
$trenTahunan = collect(range(1, 12))->map(function (int $bulan) use ($tahunDipilih, $labelBulan, $tz, $kolomWaktuMutasi) {
$mulaiBulan = Carbon::create($tahunDipilih, $bulan, 1, 0, 0, 0, $tz)->startOfMonth();
$akhirBulan = Carbon::create($tahunDipilih, $bulan, 1, 0, 0, 0, $tz)->endOfMonth();
return [
'label' => $labelBulan[$bulan],
'masuk' => (int) StockMovement::kategori(StockMovement::KATEGORI_MASUK)
->whereBetween($kolomWaktuMutasi, [$mulaiBulan, $akhirBulan])
->sum('jumlah'),
'keluar' => (int) StockMovement::kategori(StockMovement::KATEGORI_KELUAR)
->whereBetween($kolomWaktuMutasi, [$mulaiBulan, $akhirBulan])
->sum('jumlah'),
];
});
$totalMasukTahunan = (int) $trenTahunan->sum('masuk');
$totalKeluarTahunan = (int) $trenTahunan->sum('keluar');
$totalArusTahunan = $totalMasukTahunan + $totalKeluarTahunan;
if ($totalArusTahunan === 0) {
$persenMasukTahunan = 0;
$persenKeluarTahunan = 0;
} else {
$persenMasukTahunan = (int) round(($totalMasukTahunan / $totalArusTahunan) * 100);
$persenKeluarTahunan = 100 - $persenMasukTahunan;
}
$nilaiMasukPerBulan = $trenTahunan->pluck('masuk')->all();
$nilaiKeluarPerBulan = $trenTahunan->pluck('keluar')->all();
$pctMasukKontribTahunan = $totalMasukTahunan > 0
? self::distribusiPersenProporsionalTerhadapTarget($nilaiMasukPerBulan, $totalMasukTahunan, $persenMasukTahunan)
: array_fill(0, 12, null);
$pctKeluarKontribTahunan = $totalKeluarTahunan > 0
? self::distribusiPersenProporsionalTerhadapTarget($nilaiKeluarPerBulan, $totalKeluarTahunan, $persenKeluarTahunan)
: array_fill(0, 12, null);
$maksTrenTahunan = match ($jenisData) {
'masuk' => max(1, (int) $trenTahunan->max('masuk')),
'keluar' => max(1, (int) $trenTahunan->max('keluar')),
default => max(1, (int) $trenTahunan->max(fn($item) => max($item['masuk'], $item['keluar']))),
};
return view('dashboard', [
'user' => Auth::user(),
'totalProduk' => $totalProduk,
'daftarNamaProduk' => $daftarNamaProduk,
'stokMenipis' => $stokMenipis,
'jumlahBarangMasuk' => $jumlahBarangMasuk,
'jumlahBarangKeluar' => $jumlahBarangKeluar,
'jumlahBarangMasukBulanLalu' => $jumlahBarangMasukBulanLalu,
'jumlahBarangKeluarBulanLalu' => $jumlahBarangKeluarBulanLalu,
'perubahanBarangMasuk' => $perubahanBarangMasuk,
'perubahanBarangKeluar' => $perubahanBarangKeluar,
'produkStokMenipis' => $produkStokMenipis,
'aktivitasTerbaru' => $aktivitasTerbaru,
'pesanWhatsAppStokMinimum' => $pesanWhatsAppStokMinimum,
'nomorPenerimaWhatsApp' => $nomorPenerimaWhatsApp,
'persentaseStokAman' => $persentaseStokAman,
'topPergerakanProduk' => $topPergerakanProduk,
'produkTercepatTerjual' => $produkTercepatTerjual,
'produkTerlambatTerjual' => $produkTerlambatTerjual,
'produkCepatTerjualLainnya' => $produkCepatTerjualLainnya,
'produkLambatTerjualLainnya' => $produkLambatTerjualLainnya,
'batasSelisihProdukMirip' => $batasSelisihProdukMirip,
'selisihProdukTerjual' => $selisihProdukTerjual,
'labelBulanPerbandinganPenjualan' => $labelBulanPerbandinganPenjualan,
'tahunDipilih' => $tahunDipilih,
'daftarTahun' => $daftarTahun,
'trenTahunan' => $trenTahunan,
'maksTrenTahunan' => $maksTrenTahunan,
'totalMasukTahunan' => $totalMasukTahunan,
'totalKeluarTahunan' => $totalKeluarTahunan,
'jenisData' => $jenisData,
'persenMasukTahunan' => $persenMasukTahunan,
'persenKeluarTahunan' => $persenKeluarTahunan,
'pctMasukKontribTahunan' => $pctMasukKontribTahunan,
'pctKeluarKontribTahunan' => $pctKeluarKontribTahunan,
]);
}
/**
* Bagi angka pembulatan $targetPersen ke tiap kolom secara proporsional terhadap
* total jenis datanya ($totalJenis) agar jumlah hasil tepat sama dengan $targetPersen.
*
* @param array<int, int|float> $nilaiPerBulan
* @return array<int, int|null>|array<int, int>
*/
private static function distribusiPersenProporsionalTerhadapTarget(array $nilaiPerBulan, int $totalJenis, int $targetPersen): array
{
if ($totalJenis <= 0) {
return array_map(fn() => null, $nilaiPerBulan);
}
if ($targetPersen <= 0) {
return array_map(fn() => 0, $nilaiPerBulan);
}
$raw = array_map(fn($v) => ((float) $v / $totalJenis) * $targetPersen, $nilaiPerBulan);
$out = array_map(fn($v) => (int) floor($v), $raw);
$sisa = $targetPersen - array_sum($out);
$urutan = array_keys($nilaiPerBulan);
usort($urutan, function (int $a, int $b) use ($raw): int {
$fa = $raw[$a] - floor($raw[$a]);
$fb = $raw[$b] - floor($raw[$b]);
return $fb <=> $fa;
});
$jumlahKolom = count($urutan);
for ($i = 0; $i < $sisa; $i++) {
$out[$urutan[$i % $jumlahKolom]]++;
}
return $out;
}
}