MIF_E31231972/app/Http/Controllers/StockMovementController.php

290 lines
9.4 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\IncomingStock;
use App\Models\LostStock;
use App\Models\OutgoingStock;
use App\Models\StockMovement;
use App\Models\StockActivity;
use App\Services\WhatsAppStockNotifier;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class StockMovementController extends Controller
{
public function __construct(private readonly WhatsAppStockNotifier $whatsAppStockNotifier)
{
}
// public function incoming(): View
// {
// $this->ensureAdmin();
// return view('mutasi-stok.barang-masuk', [
// 'products' => Product::orderBy('nama')->get(),
// 'movements' => StockMovement::with(['product', 'user'])
// ->where('tipe', 'masuk')
// ->latest(StockMovement::columnDibuat())
// ->limit(50)
// ->get(),
// ]);
// }
public function incoming(Request $request): View
{
$this->ensureAdminOrOwner();
$query = IncomingStock::with(['product', 'user'])
->latest(IncomingStock::CREATED_AT);
if ($request->filled('tanggal_mulai')) {
$query->whereDate(IncomingStock::CREATED_AT, '>=', $request->tanggal_mulai);
}
if ($request->filled('tanggal_selesai')) {
$query->whereDate(IncomingStock::CREATED_AT, '<=', $request->tanggal_selesai);
}
if (!$request->filled('tanggal_mulai') && !$request->filled('tanggal_selesai')) {
$query->limit(50);
}
return view('mutasi-stok.barang-masuk', [
'products' => Product::orderBy('nama')->get(),
'movements' => $query->get(),
]);
}
// public function outgoing(): View
// {
// $this->ensureAdmin();
// return view('mutasi-stok.barang-keluar', [
// 'products' => Product::orderBy('nama')->get(),
// 'movements' => StockMovement::with(['product', 'user'])
// ->where('tipe', 'keluar')
// ->latest(StockMovement::columnDibuat())
// ->limit(50)
// ->get(),
// ]);
// }
public function outgoing(Request $request): View
{
$this->ensureAdminOrOwner();
$query = OutgoingStock::with(['product', 'user'])
->latest(OutgoingStock::CREATED_AT);
if ($request->filled('tanggal_mulai')) {
$query->whereDate(OutgoingStock::CREATED_AT, '>=', $request->tanggal_mulai);
}
if ($request->filled('tanggal_selesai')) {
$query->whereDate(OutgoingStock::CREATED_AT, '<=', $request->tanggal_selesai);
}
if (!$request->filled('tanggal_mulai') && !$request->filled('tanggal_selesai')) {
$query->limit(50);
}
return view('mutasi-stok.barang-keluar', [
'products' => Product::orderBy('nama')->get(),
'movements' => $query->get(),
]);
}
public function lost(): View
{
$this->ensureAdmin();
return view('mutasi-stok.barang-hilang', [
'products' => Product::orderBy('nama')->get(),
'movements' => LostStock::with(['product', 'user'])
->latest(LostStock::CREATED_AT)
->limit(50)
->get(),
]);
}
public function storeIncoming(Request $request): RedirectResponse
{
$this->ensureAdmin();
$this->saveMovement($request, 'masuk');
return redirect()->route('stock-movements.incoming')->with('success', 'Barang masuk berhasil disimpan.');
}
public function storeOutgoing(Request $request): RedirectResponse
{
$this->ensureAdmin();
$minimumStockNotificationSent = $this->saveMovement($request, 'keluar');
if ($minimumStockNotificationSent === true) {
return redirect()
->route('stock-movements.outgoing')
->with('success', 'Barang keluar berhasil disimpan dan notifikasi stok minimum berhasil dikirim.');
}
if ($minimumStockNotificationSent === false) {
return redirect()
->route('stock-movements.outgoing')
->with('success', 'Barang keluar berhasil disimpan.')
->with('warning', 'Notifikasi WhatsApp stok minimum gagal dikirim. Pastikan perangkat WhatsApp gateway terhubung.');
}
return redirect()->route('stock-movements.outgoing')->with('success', 'Barang keluar berhasil disimpan.');
}
public function storeLost(Request $request): RedirectResponse
{
$this->ensureAdmin();
$minimumStockNotificationSent = $this->saveMovement($request, 'keluar', true);
if ($minimumStockNotificationSent === true) {
return redirect()
->route('stock-movements.lost')
->with('success', 'Input barang hilang berhasil disimpan dan notifikasi stok minimum berhasil dikirim.');
}
if ($minimumStockNotificationSent === false) {
return redirect()
->route('stock-movements.lost')
->with('success', 'Input barang hilang berhasil disimpan.')
->with('warning', 'Notifikasi WhatsApp stok minimum gagal dikirim. Pastikan perangkat WhatsApp gateway terhubung.');
}
return redirect()->route('stock-movements.lost')->with('success', 'Input barang hilang berhasil disimpan.');
}
private function saveMovement(Request $request, string $type, bool $isLost = false): ?bool
{
$rules = [
'id_barang' => ['required', 'exists:barang,id'],
'jumlah' => ['required', 'integer', 'min:1'],
];
if (!$isLost) {
$rules['kategori'] = ['required', 'string', 'max:100'];
}
if ($type === 'masuk') {
$rules['keterangan'] = ['nullable', 'string', 'max:255'];
}
if ($type === 'keluar') {
$rules['keterangan'] = ['required', 'string', 'max:255'];
}
$validated = $request->validate($rules);
$description = $this->movementDescription($validated, $type, $isLost);
$minimumStockAlertProduct = DB::transaction(function () use ($validated, $description, $type, $isLost): ?Product {
$product = Product::lockForUpdate()->findOrFail($validated['id_barang']);
if (!$isLost && strtolower(trim((string) $product->kategori)) !== strtolower(trim($validated['kategori']))) {
throw ValidationException::withMessages([
'kategori' => 'Kategori yang dipilih tidak sesuai dengan barang.',
]);
}
$user = Auth::user();
$waktu = now();
$oldStock = (int) $product->stok;
$newStock = $type === 'masuk'
? $oldStock + $validated['jumlah']
: $oldStock - $validated['jumlah'];
if ($newStock < 0) {
throw ValidationException::withMessages([
'jumlah' => 'Stok tidak mencukupi untuk transaksi keluar.',
]);
}
$movement = StockMovement::create([
'id_barang' => $product->id,
'tipe' => $type,
'kategori' => $isLost
? StockMovement::KATEGORI_HILANG
: ($type === 'masuk' ? StockMovement::KATEGORI_MASUK : StockMovement::KATEGORI_KELUAR),
'jumlah' => $validated['jumlah'],
'keterangan' => $description,
'id_pengguna' => Auth::id(),
]);
$activityModel = $this->activityModel($type, $isLost);
$activityModel::create([
'id_barang' => $product->id,
'id_mutasi_stok' => $movement->id,
'waktu' => $waktu,
'barang' => $product->nama,
'jumlah' => $validated['jumlah'],
'keterangan' => $description,
'id_pengguna' => $user?->id,
'input_oleh' => $user?->name,
]);
$product->update(['stok' => $newStock]);
$shouldSendMinimumStockAlert = $type === 'keluar'
&& $newStock <= (int) $product->stok_minimum;
if ($shouldSendMinimumStockAlert) {
$product->stok = $newStock;
return $product;
}
return null;
});
if ($minimumStockAlertProduct !== null) {
return $this->whatsAppStockNotifier->sendMinimumStockAlert(
$minimumStockAlertProduct,
(int) $validated['jumlah']
);
}
return null;
}
private function movementDescription(array $validated, string $type, bool $isLost): ?string
{
return $validated['keterangan'] ?? null;
}
/**
* @return class-string<StockActivity>
*/
private function activityModel(string $type, bool $isLost): string
{
if ($isLost) {
return LostStock::class;
}
return $type === 'masuk'
? IncomingStock::class
: OutgoingStock::class;
}
private function ensureAdmin(): void
{
abort_unless(auth()->user()?->role === 'admin', 403);
}
private function ensureAdminOrOwner(): void
{
abort_unless(in_array(auth()->user()?->role, ['admin', 'owner'], true), 403);
}
}