fix handle error and add note for shippings receive approve and reject mutations
This commit is contained in:
@@ -37,4 +37,31 @@ enum MutationStatus: string
|
||||
self::CANCELLED => 'secondary',
|
||||
};
|
||||
}
|
||||
|
||||
public function textColorClass(): string
|
||||
{
|
||||
return match($this->color()) {
|
||||
'success' => 'text-success',
|
||||
'warning' => 'text-warning',
|
||||
'danger' => 'text-danger',
|
||||
'info' => 'text-info',
|
||||
'primary' => 'text-primary',
|
||||
'brand' => 'text-primary',
|
||||
'secondary' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
}
|
||||
|
||||
public static function getOptions(): array
|
||||
{
|
||||
return [
|
||||
self::PENDING->value => self::PENDING->label(),
|
||||
self::SENT->value => self::SENT->label(),
|
||||
self::RECEIVED->value => self::RECEIVED->label(),
|
||||
self::APPROVED->value => self::APPROVED->label(),
|
||||
self::REJECTED->value => self::REJECTED->label(),
|
||||
self::COMPLETED->value => self::COMPLETED->label(),
|
||||
self::CANCELLED->value => self::CANCELLED->label(),
|
||||
];
|
||||
}
|
||||
}
|
||||
55
app/Enums/OpnameStatus.php
Normal file
55
app/Enums/OpnameStatus.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum OpnameStatus: string
|
||||
{
|
||||
case DRAFT = 'draft';
|
||||
case PENDING = 'pending';
|
||||
case APPROVED = 'approved';
|
||||
case REJECTED = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DRAFT => 'Draft',
|
||||
self::PENDING => 'Menunggu Persetujuan',
|
||||
self::APPROVED => 'Disetujui',
|
||||
self::REJECTED => 'Ditolak',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DRAFT => 'warning',
|
||||
self::PENDING => 'info',
|
||||
self::APPROVED => 'success',
|
||||
self::REJECTED => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
public function textColorClass(): string
|
||||
{
|
||||
return match($this->color()) {
|
||||
'success' => 'text-success',
|
||||
'warning' => 'text-warning',
|
||||
'danger' => 'text-danger',
|
||||
'info' => 'text-info',
|
||||
'primary' => 'text-primary',
|
||||
'brand' => 'text-primary',
|
||||
'secondary' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
}
|
||||
|
||||
public static function getOptions(): array
|
||||
{
|
||||
return [
|
||||
self::DRAFT->value => self::DRAFT->label(),
|
||||
self::PENDING->value => self::PENDING->label(),
|
||||
self::APPROVED->value => self::APPROVED->label(),
|
||||
self::REJECTED->value => self::REJECTED->label(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -45,21 +45,11 @@ class MutationsController extends Controller
|
||||
return $row->requestedBy->name ?? '-';
|
||||
})
|
||||
->addColumn('status', function($row) {
|
||||
$statusColor = $row->status_color;
|
||||
$statusLabel = $row->status_label;
|
||||
$status = $row->status instanceof MutationStatus ? $row->status : MutationStatus::from($row->status);
|
||||
$textColorClass = $status->textColorClass();
|
||||
$label = $status->label();
|
||||
|
||||
$textColorClass = match($statusColor) {
|
||||
'success' => 'text-success',
|
||||
'warning' => 'text-warning',
|
||||
'danger' => 'text-danger',
|
||||
'info' => 'text-info',
|
||||
'primary' => 'text-primary',
|
||||
'brand' => 'text-primary',
|
||||
'secondary' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$statusLabel}</span>";
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
||||
})
|
||||
->addColumn('total_items', function($row) {
|
||||
return number_format($row->total_items, 0);
|
||||
@@ -91,6 +81,7 @@ class MutationsController extends Controller
|
||||
$request->validate([
|
||||
'from_dealer_id' => 'required|exists:dealers,id',
|
||||
'to_dealer_id' => 'required|exists:dealers,id|different:from_dealer_id',
|
||||
'shipping_notes' => 'nullable|string',
|
||||
'products' => 'required|array|min:1',
|
||||
'products.*.product_id' => 'required|exists:products,id',
|
||||
'products.*.quantity_requested' => 'required|numeric|min:0.01'
|
||||
@@ -102,8 +93,9 @@ class MutationsController extends Controller
|
||||
$mutation = Mutation::create([
|
||||
'from_dealer_id' => $request->from_dealer_id,
|
||||
'to_dealer_id' => $request->to_dealer_id,
|
||||
'status' => 'sent',
|
||||
'requested_by' => auth()->id()
|
||||
'status' => MutationStatus::SENT,
|
||||
'requested_by' => auth()->id(),
|
||||
'shipping_notes' => $request->shipping_notes
|
||||
]);
|
||||
|
||||
// Buat mutation details
|
||||
@@ -134,7 +126,16 @@ class MutationsController extends Controller
|
||||
|
||||
public function show(Mutation $mutation)
|
||||
{
|
||||
$mutation->load(['fromDealer', 'toDealer', 'requestedBy.role', 'approvedBy.role', 'receivedBy.role', 'mutationDetails.product']);
|
||||
$mutation->load([
|
||||
'fromDealer',
|
||||
'toDealer',
|
||||
'requestedBy.role',
|
||||
'approvedBy.role',
|
||||
'receivedBy.role',
|
||||
'rejectedBy.role',
|
||||
'cancelledBy.role',
|
||||
'mutationDetails.product'
|
||||
]);
|
||||
|
||||
return view('warehouse_management.mutations.show', compact('mutation'));
|
||||
}
|
||||
@@ -142,7 +143,7 @@ class MutationsController extends Controller
|
||||
public function receive(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'notes' => 'nullable|string',
|
||||
'reception_notes' => 'nullable|string',
|
||||
'products' => 'required|array',
|
||||
'products.*.quantity_approved' => 'required|numeric|min:0',
|
||||
'products.*.notes' => 'nullable|string'
|
||||
@@ -154,11 +155,6 @@ class MutationsController extends Controller
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Update mutation notes jika ada
|
||||
if ($request->notes) {
|
||||
$mutation->update(['notes' => $request->notes]);
|
||||
}
|
||||
|
||||
// Update product details dengan quantity_approved dan notes
|
||||
if ($request->products) {
|
||||
foreach ($request->products as $detailId => $productData) {
|
||||
@@ -182,8 +178,8 @@ class MutationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Receive mutation
|
||||
$mutation->receive(auth()->id());
|
||||
// Receive mutation with reception notes
|
||||
$mutation->receive(auth()->id(), $request->reception_notes);
|
||||
|
||||
DB::commit();
|
||||
return redirect()->route('mutations.index')
|
||||
@@ -198,7 +194,7 @@ class MutationsController extends Controller
|
||||
public function approve(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'notes' => 'nullable|string'
|
||||
'approval_notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeApproved()) {
|
||||
@@ -207,7 +203,7 @@ class MutationsController extends Controller
|
||||
|
||||
try {
|
||||
// Approve mutation (quantity_approved sudah diisi saat receive)
|
||||
$mutation->approve(auth()->id(), $request->notes);
|
||||
$mutation->approve(auth()->id(), $request->approval_notes);
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil disetujui');
|
||||
@@ -255,14 +251,18 @@ class MutationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function cancel(Mutation $mutation)
|
||||
public function cancel(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'cancellation_reason' => 'nullable|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeCancelled()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat dibatalkan dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->update(['status' => MutationStatus::CANCELLED]);
|
||||
$mutation->cancel(auth()->id(), $request->cancellation_reason);
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil dibatalkan');
|
||||
@@ -349,6 +349,8 @@ class MutationsController extends Controller
|
||||
'requestedBy.role',
|
||||
'approvedBy.role',
|
||||
'receivedBy.role',
|
||||
'rejectedBy.role',
|
||||
'cancelledBy.role',
|
||||
'mutationDetails.product'
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Enums\OpnameStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Menu;
|
||||
@@ -39,30 +40,16 @@ class OpnamesController extends Controller
|
||||
return Carbon::parse($row->created_at)->format('d M Y H:i');
|
||||
})
|
||||
->editColumn('status', function ($row) {
|
||||
$statusColor = [
|
||||
'draft' => 'warning',
|
||||
'pending' => 'info',
|
||||
'approved' => 'success',
|
||||
'rejected' => 'danger'
|
||||
][$row->status] ?? 'secondary';
|
||||
$status = $row->status instanceof OpnameStatus ? $row->status : OpnameStatus::from($row->status);
|
||||
$textColorClass = $status->textColorClass();
|
||||
$label = $status->label();
|
||||
|
||||
$textColorClass = match($statusColor) {
|
||||
'success' => 'text-success',
|
||||
'warning' => 'text-warning',
|
||||
'danger' => 'text-danger',
|
||||
'info' => 'text-info',
|
||||
'primary' => 'text-primary',
|
||||
'brand' => 'text-primary',
|
||||
'secondary' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">" . ucfirst($row->status) . "</span>";
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
||||
})
|
||||
->addColumn('action', function ($row) use ($menu) {
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
$btn .= '<a href="'.route('opnames.show', $row->id).'" class="btn btn-secondary btn-sm">Detail</a>';
|
||||
$btn .= '<a href="'.route('opnames.show', $row->id).'" class="btn btn-primary btn-sm">Detail</a>';
|
||||
|
||||
$btn .= '</div>';
|
||||
|
||||
@@ -253,7 +240,7 @@ class OpnamesController extends Controller
|
||||
'opname_date' => $opnameDate,
|
||||
'user_id' => $userId,
|
||||
'note' => $note,
|
||||
'status' => 'approved', // Set status langsung approved
|
||||
'status' => OpnameStatus::APPROVED, // Set status langsung approved
|
||||
'approved_by' => $userId, // Set current user sebagai approver
|
||||
'approved_at' => now() // Set waktu approval
|
||||
]);
|
||||
|
||||
@@ -20,16 +20,25 @@ class Mutation extends Model
|
||||
'requested_by',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'approval_notes',
|
||||
'received_by',
|
||||
'received_at',
|
||||
'notes',
|
||||
'rejection_reason'
|
||||
'reception_notes',
|
||||
'shipping_notes',
|
||||
'rejection_reason',
|
||||
'rejected_by',
|
||||
'rejected_at',
|
||||
'cancelled_by',
|
||||
'cancelled_at',
|
||||
'cancellation_reason'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => MutationStatus::class,
|
||||
'approved_at' => 'datetime',
|
||||
'received_at' => 'datetime'
|
||||
'received_at' => 'datetime',
|
||||
'rejected_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
@@ -66,6 +75,16 @@ class Mutation extends Model
|
||||
return $this->belongsTo(User::class, 'received_by');
|
||||
}
|
||||
|
||||
public function rejectedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'rejected_by');
|
||||
}
|
||||
|
||||
public function cancelledBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
|
||||
public function mutationDetails()
|
||||
{
|
||||
return $this->hasMany(MutationDetail::class);
|
||||
@@ -137,7 +156,7 @@ class Mutation extends Model
|
||||
}
|
||||
|
||||
// Receive mutation by destination dealer
|
||||
public function receive($userId)
|
||||
public function receive($userId, $receptionNotes = null)
|
||||
{
|
||||
if (!$this->canBeReceived()) {
|
||||
throw new \Exception('Mutasi tidak dapat diterima dalam status saat ini');
|
||||
@@ -146,14 +165,15 @@ class Mutation extends Model
|
||||
$this->update([
|
||||
'status' => MutationStatus::RECEIVED,
|
||||
'received_by' => $userId,
|
||||
'received_at' => now()
|
||||
'received_at' => now(),
|
||||
'reception_notes' => $receptionNotes
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Approve mutation
|
||||
public function approve($userId, $notes = null)
|
||||
public function approve($userId, $approvalNotes = null)
|
||||
{
|
||||
if (!$this->canBeApproved()) {
|
||||
throw new \Exception('Mutasi tidak dapat disetujui dalam status saat ini');
|
||||
@@ -163,7 +183,7 @@ class Mutation extends Model
|
||||
'status' => MutationStatus::APPROVED,
|
||||
'approved_by' => $userId,
|
||||
'approved_at' => now(),
|
||||
'notes' => $notes
|
||||
'approval_notes' => $approvalNotes
|
||||
]);
|
||||
|
||||
return $this;
|
||||
@@ -178,14 +198,31 @@ class Mutation extends Model
|
||||
|
||||
$this->update([
|
||||
'status' => MutationStatus::REJECTED,
|
||||
'approved_by' => $userId,
|
||||
'approved_at' => now(),
|
||||
'rejected_by' => $userId,
|
||||
'rejected_at' => now(),
|
||||
'rejection_reason' => $rejectionReason
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Cancel mutation
|
||||
public function cancel($userId, $cancellationReason = null)
|
||||
{
|
||||
if (!$this->canBeCancelled()) {
|
||||
throw new \Exception('Mutasi tidak dapat dibatalkan dalam status saat ini');
|
||||
}
|
||||
|
||||
$this->update([
|
||||
'status' => MutationStatus::CANCELLED,
|
||||
'cancelled_by' => $userId,
|
||||
'cancelled_at' => now(),
|
||||
'cancellation_reason' => $cancellationReason
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Complete mutation (actually move the stock)
|
||||
public function complete()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\OpnameStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -22,14 +23,15 @@ class Opname extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'approved_at' => 'datetime'
|
||||
'approved_at' => 'datetime',
|
||||
'status' => OpnameStatus::class
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::updated(function ($opname) {
|
||||
// Jika status berubah menjadi approved
|
||||
if ($opname->isDirty('status') && $opname->status === 'approved') {
|
||||
if ($opname->isDirty('status') && $opname->status === OpnameStatus::APPROVED) {
|
||||
// Update stock untuk setiap detail opname
|
||||
foreach ($opname->details as $detail) {
|
||||
$stock = Stock::firstOrCreate(
|
||||
@@ -74,11 +76,11 @@ class Opname extends Model
|
||||
// Method untuk approve opname
|
||||
public function approve(User $approver)
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
if ($this->status !== OpnameStatus::PENDING) {
|
||||
throw new \Exception('Only pending opnames can be approved');
|
||||
}
|
||||
|
||||
$this->status = 'approved';
|
||||
$this->status = OpnameStatus::APPROVED;
|
||||
$this->approved_by = $approver->id;
|
||||
$this->approved_at = now();
|
||||
$this->save();
|
||||
@@ -89,11 +91,11 @@ class Opname extends Model
|
||||
// Method untuk reject opname
|
||||
public function reject(User $rejector, string $note)
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
if ($this->status !== OpnameStatus::PENDING) {
|
||||
throw new \Exception('Only pending opnames can be rejected');
|
||||
}
|
||||
|
||||
$this->status = 'rejected';
|
||||
$this->status = OpnameStatus::REJECTED;
|
||||
$this->approved_by = $rejector->id;
|
||||
$this->approved_at = now();
|
||||
$this->rejection_note = $note;
|
||||
@@ -105,11 +107,11 @@ class Opname extends Model
|
||||
// Method untuk submit opname untuk approval
|
||||
public function submit()
|
||||
{
|
||||
if ($this->status !== 'draft') {
|
||||
if ($this->status !== OpnameStatus::DRAFT) {
|
||||
throw new \Exception('Only draft opnames can be submitted');
|
||||
}
|
||||
|
||||
$this->status = 'pending';
|
||||
$this->status = OpnameStatus::PENDING;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
|
||||
Reference in New Issue
Block a user