Files
CKB/app/Http/Controllers/WarehouseManagement/MutationsController.php

502 lines
22 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\WarehouseManagement;
use App\Http\Controllers\Controller;
use App\Models\Mutation;
use App\Models\MutationDetail;
use App\Models\Product;
use App\Models\Dealer;
use App\Enums\MutationStatus;
use App\Models\Menu;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Yajra\DataTables\DataTables;
class MutationsController extends Controller
{
public function index(Request $request)
{
$menu = Menu::where('link','mutations.index')->first();
if ($request->ajax()) {
// Use a more specific query to avoid join conflicts
$data = Mutation::query()
->with(['fromDealer', 'toDealer', 'requestedBy.role', 'approvedBy.role', 'receivedBy.role'])
->select([
'mutations.*'
])
->orderBy('mutations.id', 'desc'); // Default order by ID desc
// Filter berdasarkan dealer jika user bukan admin
if (auth()->user()->dealer_id) {
$data->where(function($query) {
$query->where('from_dealer_id', auth()->user()->dealer_id)
->orWhere('to_dealer_id', auth()->user()->dealer_id);
});
}
return DataTables::of($data)
->addIndexColumn()
->addColumn('mutation_number', function($row) {
return $row->mutation_number;
})
->addColumn('from_dealer', function($row) {
return $row->fromDealer->name ?? '-';
})
->addColumn('to_dealer', function($row) {
return $row->toDealer->name ?? '-';
})
->addColumn('requested_by', function($row) {
return $row->requestedBy->name ?? '-';
})
->addColumn('status', function($row) {
$status = $row->status instanceof MutationStatus ? $row->status : MutationStatus::from($row->status);
$textColorClass = $status->textColorClass();
$label = $status->label();
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
})
->addColumn('total_items', function($row) {
return number_format($row->total_items, 0);
})
->addColumn('created_at', function($row) {
return $row->created_at->format('d/m/Y H:i');
})
->addColumn('action', function($row) {
return view('warehouse_management.mutations._action', compact('row'))->render();
})
// Enhanced filtering
->filterColumn('mutation_number', function($query, $keyword) {
$query->where('mutations.mutation_number', 'like', "%{$keyword}%");
})
->filterColumn('from_dealer', function($query, $keyword) {
$query->whereHas('fromDealer', function($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%");
});
})
->filterColumn('to_dealer', function($query, $keyword) {
$query->whereHas('toDealer', function($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%");
});
})
->filterColumn('requested_by', function($query, $keyword) {
$query->whereHas('requestedBy', function($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%");
});
})
->filterColumn('status', function($query, $keyword) {
$query->where('mutations.status', 'like', "%{$keyword}%");
})
->filterColumn('created_at', function($query, $keyword) {
$query->whereDate('mutations.created_at', 'like', "%{$keyword}%");
})
// Enhanced ordering - avoid join conflicts by using subqueries
->orderColumn('mutation_number', function($query, $order) {
$query->orderBy('mutations.mutation_number', $order);
})
->orderColumn('from_dealer', function($query, $order) {
$query->orderBy(
DB::raw('(SELECT name FROM dealers WHERE dealers.id = mutations.from_dealer_id)'),
$order
);
})
->orderColumn('to_dealer', function($query, $order) {
$query->orderBy(
DB::raw('(SELECT name FROM dealers WHERE dealers.id = mutations.to_dealer_id)'),
$order
);
})
->orderColumn('requested_by', function($query, $order) {
$query->orderBy(
DB::raw('(SELECT name FROM users WHERE users.id = mutations.requested_by)'),
$order
);
})
->orderColumn('total_items', function($query, $order) {
$query->orderBy(
DB::raw('(SELECT SUM(quantity_requested) FROM mutation_details WHERE mutation_details.mutation_id = mutations.id)'),
$order
);
})
->orderColumn('status', function($query, $order) {
$query->orderBy('mutations.status', $order);
})
->orderColumn('created_at', function($query, $order) {
$query->orderBy('mutations.created_at', $order);
})
->rawColumns(['status', 'action'])
->make(true);
}
return view('warehouse_management.mutations.index', compact('menu'));
}
public function create()
{
$menu = Menu::where('link','mutations.create')->first();
$dealers = Dealer::all();
$products = Product::with('stocks')->get();
return view('warehouse_management.mutations.create', compact('menu', 'dealers', 'products'));
}
public function store(Request $request)
{
$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'
]);
DB::beginTransaction();
try {
// Buat mutation record dengan status SENT (langsung terkirim ke dealer tujuan)
$mutation = Mutation::create([
'from_dealer_id' => $request->from_dealer_id,
'to_dealer_id' => $request->to_dealer_id,
'status' => MutationStatus::SENT,
'requested_by' => auth()->id(),
'shipping_notes' => $request->shipping_notes
]);
// Buat mutation details
foreach ($request->products as $productData) {
MutationDetail::create([
'mutation_id' => $mutation->id,
'product_id' => $productData['product_id'],
'quantity_requested' => $productData['quantity_requested']
]);
}
DB::commit();
// Check if request came from transaction page
if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) {
return redirect()->back()
->with('success', 'Mutasi berhasil dibuat dan terkirim ke dealer tujuan');
}
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil dibuat dan terkirim ke dealer tujuan');
} catch (\Exception $e) {
DB::rollback();
return back()->withErrors(['error' => 'Gagal membuat mutasi: ' . $e->getMessage()]);
}
}
public function show(Mutation $mutation)
{
$mutation->load([
'fromDealer',
'toDealer',
'requestedBy.role',
'approvedBy.role',
'receivedBy.role',
'rejectedBy.role',
'cancelledBy.role',
'mutationDetails.product'
]);
return view('warehouse_management.mutations.show', compact('mutation'));
}
public function receive(Request $request, Mutation $mutation)
{
$request->validate([
'reception_notes' => 'nullable|string',
'products' => 'required|array',
'products.*.quantity_approved' => 'required|numeric|min:0',
'products.*.notes' => 'nullable|string'
]);
if (!$mutation->canBeReceived()) {
return back()->withErrors(['error' => 'Mutasi tidak dapat diterima dalam status saat ini']);
}
DB::beginTransaction();
try {
// Update product details dengan quantity_approved dan notes
if ($request->products) {
foreach ($request->products as $detailId => $productData) {
$updateData = [];
// Set quantity_approved
if (isset($productData['quantity_approved'])) {
$updateData['quantity_approved'] = $productData['quantity_approved'];
}
// Set notes jika ada
if (isset($productData['notes']) && !empty($productData['notes'])) {
$updateData['notes'] = $productData['notes'];
}
if (!empty($updateData)) {
MutationDetail::where('id', $detailId)
->where('mutation_id', $mutation->id)
->update($updateData);
}
}
}
// Receive mutation with reception notes
$mutation->receive(auth()->id(), $request->reception_notes);
DB::commit();
// Check user role and redirect accordingly
if (!auth()->user()->dealer_id) {
// Users without dealer_id are likely admin, redirect to mutations index
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil diterima dan siap untuk disetujui. Stock akan dipindahkan setelah disetujui.');
} else {
// Dealer users redirect back to transaction page
return redirect()->route('transaction')
->with('success', 'Mutasi berhasil diterima. Silakan setujui mutasi ini untuk memindahkan stock.')
->with('active_tab', 'penerimaan');
}
} catch (\Exception $e) {
DB::rollback();
return back()->withErrors(['error' => 'Gagal menerima mutasi: ' . $e->getMessage()]);
}
}
public function approve(Request $request, Mutation $mutation)
{
$request->validate([
'approval_notes' => 'nullable|string'
]);
if (!$mutation->canBeApproved()) {
return back()->withErrors(['error' => 'Mutasi tidak dapat disetujui dalam status saat ini']);
}
try {
// Approve mutation (stock will move automatically)
$mutation->approve(auth()->id(), $request->approval_notes);
// Check user role and redirect accordingly
if (!auth()->user()->dealer_id) {
// Admin users redirect to mutations index
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan');
} else {
// Dealer users
if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) {
return redirect()->route('transaction')
->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan')
->with('active_tab', 'penerimaan');
} else {
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan');
}
}
} catch (\Exception $e) {
return back()->withErrors(['error' => 'Gagal menyetujui mutasi: ' . $e->getMessage()]);
}
}
public function reject(Request $request, Mutation $mutation)
{
$request->validate([
'rejection_reason' => 'required|string'
]);
if (!$mutation->canBeApproved()) {
return back()->withErrors(['error' => 'Mutasi tidak dapat ditolak dalam status saat ini']);
}
try {
$mutation->reject(auth()->id(), $request->rejection_reason);
// Check user role and redirect accordingly
if (!auth()->user()->dealer_id) {
// Admin users redirect to mutations index
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil ditolak');
} else {
// Dealer users
if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) {
return redirect()->route('transaction')
->with('success', 'Mutasi berhasil ditolak')
->with('active_tab', 'penerimaan');
} else {
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil ditolak');
}
}
} catch (\Exception $e) {
return back()->withErrors(['error' => 'Gagal menolak mutasi: ' . $e->getMessage()]);
}
}
// Complete method removed - Stock moves automatically after approval
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->cancel(auth()->id(), $request->cancellation_reason);
return redirect()->route('mutations.index')
->with('success', 'Mutasi berhasil dibatalkan');
} catch (\Exception $e) {
return back()->withErrors(['error' => 'Gagal membatalkan mutasi: ' . $e->getMessage()]);
}
}
// API untuk mendapatkan stock produk di dealer tertentu
public function getProductStock(Request $request)
{
$dealerId = $request->dealer_id;
$productId = $request->product_id;
$product = Product::findOrFail($productId);
$stock = $product->getStockByDealer($dealerId);
return response()->json([
'product_name' => $product->name,
'current_stock' => $stock
]);
}
// API untuk mendapatkan mutasi yang perlu diterima oleh dealer
public function getPendingMutations(Request $request)
{
$dealerId = $request->dealer_id;
// Get mutations that need action from this dealer:
// 1. 'sent' status where this dealer is the recipient (need to receive)
// 2. 'received' status where this dealer is the recipient (show as waiting for admin approval)
// 3. 'approved' status where this dealer is the recipient (show as completed)
// 4. 'rejected' status where this dealer is the recipient (show as rejected)
$data = Mutation::with(['fromDealer', 'toDealer', 'requestedBy.role'])
->where(function($query) use ($dealerId) {
// Mutations sent to this dealer that need to be received
$query->where('to_dealer_id', $dealerId)
->where('status', 'sent');
// OR mutations received by this dealer (waiting for admin approval)
$query->orWhere(function($subQuery) use ($dealerId) {
$subQuery->where('to_dealer_id', $dealerId)
->where('status', 'received');
});
// OR mutations approved/rejected for this dealer (historical data)
$query->orWhere(function($subQuery) use ($dealerId) {
$subQuery->where('to_dealer_id', $dealerId)
->whereIn('status', ['approved', 'rejected']);
});
})
->orderBy('mutations.id', 'desc'); // Default order by ID desc
return DataTables::of($data)
->addIndexColumn()
->addColumn('mutation_number', function($row) {
return $row->mutation_number;
})
->addColumn('from_dealer', function($row) {
return $row->fromDealer->name ?? '-';
})
->addColumn('to_dealer', function($row) {
return $row->toDealer->name ?? '-';
})
->addColumn('status', function($row) {
$status = $row->status instanceof MutationStatus ? $row->status : MutationStatus::from($row->status);
$textColorClass = $status->textColorClass();
$label = $status->label();
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
})
->addColumn('total_items', function($row) {
return number_format($row->total_items, 0);
})
->addColumn('created_at', function($row) {
return $row->created_at->format('d/m/Y H:i');
})
->addColumn('action', function($row) use ($dealerId) {
$buttons = '';
if ($row->status->value === 'sent' && $row->to_dealer_id == $dealerId) {
// For sent mutations where current dealer is recipient - show detail button for receiving
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
Detail & Terima
</button>';
} elseif ($row->status->value === 'received' && $row->to_dealer_id == $dealerId) {
// For received mutations where current dealer is recipient - only show detail (approval is admin only)
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
Detail
</button>';
$buttons .= '<div class="mt-1"><small class="text-muted">Menunggu persetujuan admin</small></div>';
} elseif ($row->status->value === 'approved' && $row->to_dealer_id == $dealerId) {
// For approved mutations - show detail only
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
Detail
</button>';
$buttons .= '<div class="mt-1"><small class="text-success">Disetujui admin</small></div>';
} elseif ($row->status->value === 'rejected' && $row->to_dealer_id == $dealerId) {
// For rejected mutations - show detail only
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
Detail
</button>';
$buttons .= '<div class="mt-1"><small class="text-danger">Ditolak admin</small></div>';
}
return $buttons;
})
->rawColumns(['status', 'action'])
->make(true);
}
// API untuk mendapatkan detail mutasi
public function getDetail(Mutation $mutation)
{
try {
$mutation->load([
'fromDealer',
'toDealer',
'requestedBy.role',
'approvedBy.role',
'receivedBy.role',
'rejectedBy.role',
'cancelledBy.role',
'mutationDetails.product'
]);
// Format created_at
$mutation->created_at_formatted = $mutation->created_at->format('d/m/Y H:i');
// Add status color and label
$mutation->status_color = $mutation->status_color;
$mutation->status_label = $mutation->status_label;
// Check if can be received
$mutation->can_be_received = $mutation->canBeReceived();
return response()->json([
'success' => true,
'data' => $mutation
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal memuat detail mutasi: ' . $e->getMessage()
], 500);
}
}
}