498 lines
21 KiB
PHP
Executable File
498 lines
21 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\WarehouseManagement;
|
|
|
|
use App\Enums\OpnameStatus;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Dealer;
|
|
use App\Models\Menu;
|
|
use App\Models\Opname;
|
|
use App\Models\OpnameDetail;
|
|
use App\Models\Product;
|
|
use App\Models\Stock;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Validation\Rule;
|
|
use Yajra\DataTables\Facades\DataTables;
|
|
use Illuminate\Support\Facades\Gate;
|
|
|
|
class OpnamesController extends Controller
|
|
{
|
|
public function index(Request $request){
|
|
$menu = Menu::where('link','opnames.index')->first();
|
|
abort_if(!Gate::allows('view', $menu), 403);
|
|
$dealers = Dealer::all();
|
|
if($request->ajax()){
|
|
$data = Opname::query()
|
|
->with('user','dealer')
|
|
->orderBy('created_at', 'desc');
|
|
|
|
// Filter berdasarkan dealer yang dipilih
|
|
if ($request->filled('dealer_filter')) {
|
|
$data->where('dealer_id', $request->dealer_filter);
|
|
}
|
|
|
|
// Filter berdasarkan tanggal
|
|
if ($request->filled('date_from')) {
|
|
try {
|
|
$dateFrom = \Carbon\Carbon::parse($request->date_from)->format('Y-m-d');
|
|
$data->whereDate('opname_date', '>=', $dateFrom);
|
|
} catch (\Exception $e) {
|
|
// Fallback to original format
|
|
$data->whereDate('opname_date', '>=', $request->date_from);
|
|
}
|
|
}
|
|
|
|
if ($request->filled('date_to')) {
|
|
try {
|
|
$dateTo = \Carbon\Carbon::parse($request->date_to)->format('Y-m-d');
|
|
$data->whereDate('opname_date', '<=', $dateTo);
|
|
} catch (\Exception $e) {
|
|
// Fallback to original format
|
|
$data->whereDate('opname_date', '<=', $request->date_to);
|
|
}
|
|
}
|
|
|
|
return DataTables::of($data)
|
|
->addColumn('user_name', function ($row){
|
|
return $row->user ? $row->user->name : '-';
|
|
})
|
|
->addColumn('dealer_name', function ($row){
|
|
return $row->dealer ? $row->dealer->name : '-';
|
|
})
|
|
->editColumn('opname_date', function ($row){
|
|
return $row->opname_date ? Carbon::parse($row->opname_date)->format('d M Y') : '-';
|
|
})
|
|
->editColumn('created_at', function ($row) {
|
|
return Carbon::parse($row->created_at)->format('d M Y H:i');
|
|
})
|
|
->editColumn('status', function ($row) {
|
|
$status = $row->status instanceof OpnameStatus ? $row->status : OpnameStatus::from($row->status);
|
|
$textColorClass = $status->textColorClass();
|
|
$label = $status->label();
|
|
|
|
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-primary btn-sm" style="margin-right: 8px;">Detail</a>';
|
|
$btn .= '<a href="'.route('opnames.print', $row->id).'" class="btn btn-success btn-sm" target="_blank">Print</a>';
|
|
|
|
$btn .= '</div>';
|
|
|
|
return $btn;
|
|
})
|
|
->rawColumns(['action', 'status'])
|
|
->make(true);
|
|
}
|
|
|
|
return view('warehouse_management.opnames.index', compact('dealers'));
|
|
}
|
|
|
|
public function create(){
|
|
try{
|
|
$dealers = Dealer::all();
|
|
$products = Product::where('active', true)->get();
|
|
|
|
// Get initial stock data for the first dealer (if any)
|
|
$initialDealerId = $dealers->first()?->id;
|
|
$stocks = [];
|
|
if ($initialDealerId) {
|
|
$stocks = Stock::where('dealer_id', $initialDealerId)
|
|
->whereIn('product_id', $products->pluck('id'))
|
|
->get()
|
|
->keyBy('product_id');
|
|
}
|
|
|
|
return view('warehouse_management.opnames.create', compact('dealers', 'products', 'stocks'));
|
|
} catch(\Exception $ex) {
|
|
Log::error($ex->getMessage());
|
|
return back()->with('error', 'Terjadi kesalahan saat memuat data');
|
|
}
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// Check if this is from transaction form or regular opname form
|
|
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
|
|
|
if ($isTransactionForm) {
|
|
// Custom validation for transaction form
|
|
$request->validate([
|
|
'dealer_id' => 'required|exists:dealers,id',
|
|
'user_id' => 'required|exists:users,id',
|
|
'opname_date' => [
|
|
'nullable',
|
|
'string',
|
|
'date_format:Y-m-d',
|
|
'before_or_equal:today'
|
|
],
|
|
'description' => 'nullable|string|max:1000',
|
|
'product_id' => 'required|array|min:1',
|
|
'product_id.*' => 'required|exists:products,id',
|
|
'system_stock' => 'required|array',
|
|
'system_stock.*' => 'required|numeric|min:0',
|
|
'physical_stock' => 'required|array',
|
|
'physical_stock.*' => 'required|numeric|min:0'
|
|
]);
|
|
|
|
// Process transaction form data with proper date parsing
|
|
$dealerId = $request->dealer_id;
|
|
$userId = $request->user_id;
|
|
|
|
// Parse opname date (YYYY-MM-DD format) or use today if empty
|
|
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
|
Log::info('Parsing opname date', ['input' => $request->opname_date, 'using' => $inputDate]);
|
|
$opnameDate = Carbon::createFromFormat('Y-m-d', $inputDate);
|
|
Log::info('Successfully parsed opname date', ['parsed' => $opnameDate->format('Y-m-d')]);
|
|
|
|
$note = $request->description;
|
|
$productIds = $request->product_id;
|
|
$systemStocks = $request->system_stock;
|
|
$physicalStocks = $request->physical_stock;
|
|
|
|
// Log input data untuk debugging
|
|
Log::info('Transaction form input data', [
|
|
'product_ids' => $productIds,
|
|
'system_stocks' => $systemStocks,
|
|
'physical_stocks' => $physicalStocks,
|
|
'dealer_id' => $dealerId,
|
|
'user_id' => $userId
|
|
]);
|
|
|
|
} else {
|
|
// Original validation for regular opname form
|
|
$request->validate([
|
|
'dealer' => 'required|exists:dealers,id',
|
|
'product' => 'required|array|min:1',
|
|
'product.*' => 'required|exists:products,id',
|
|
'system_quantity' => 'required|array',
|
|
'system_quantity.*' => 'required|numeric|min:0',
|
|
'physical_quantity' => 'required|array',
|
|
'physical_quantity.*' => 'required|numeric|min:0',
|
|
'note' => 'nullable|string|max:1000',
|
|
'item_notes' => 'nullable|array',
|
|
'item_notes.*' => 'nullable|string|max:255',
|
|
'opname_date' => 'nullable|date' // Add opname_date validation for regular form
|
|
]);
|
|
|
|
// Process regular form data
|
|
$dealerId = $request->dealer;
|
|
$userId = auth()->id();
|
|
|
|
// Use provided date or current date
|
|
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
|
$opnameDate = $request->opname_date ?
|
|
Carbon::createFromFormat('Y-m-d', $inputDate) :
|
|
now();
|
|
|
|
$note = $request->note;
|
|
$productIds = $request->product;
|
|
$systemStocks = $request->system_quantity;
|
|
$physicalStocks = $request->physical_quantity;
|
|
}
|
|
|
|
// 2. Validasi minimal ada produk yang diisi (termasuk nilai 0)
|
|
$validProductIds = array_filter($productIds);
|
|
$validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; });
|
|
$validPhysicalStocks = array_filter($physicalStocks, function($value) {
|
|
return $value !== null && $value !== '' && is_numeric($value);
|
|
});
|
|
|
|
if (empty($validProductIds) || count($validProductIds) === 0) {
|
|
throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.');
|
|
}
|
|
|
|
if (count($validPhysicalStocks) === 0) {
|
|
throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).');
|
|
}
|
|
|
|
// 3. Validasi duplikasi produk
|
|
$productCounts = array_count_values($validProductIds);
|
|
foreach ($productCounts as $productId => $count) {
|
|
if ($count > 1) {
|
|
throw new \Exception('Produk tidak boleh duplikat dalam satu opname.');
|
|
}
|
|
}
|
|
|
|
// 4. Validasi dealer
|
|
$dealer = Dealer::findOrFail($dealerId);
|
|
|
|
// 5. Validasi user exists
|
|
$user = User::findOrFail($userId);
|
|
|
|
// 6. Validasi produk aktif
|
|
$filteredProductIds = array_filter($productIds);
|
|
$inactiveProducts = Product::whereIn('id', $filteredProductIds)
|
|
->where('active', false)
|
|
->pluck('name')
|
|
->toArray();
|
|
|
|
if (!empty($inactiveProducts)) {
|
|
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
|
}
|
|
|
|
// 7. Validasi stock difference (for transaction form, we'll allow any difference without note requirement)
|
|
$stockDifferences = [];
|
|
if (!$isTransactionForm) {
|
|
// Only validate notes for regular opname form
|
|
foreach ($productIds as $index => $productId) {
|
|
if (!$productId) continue;
|
|
|
|
$systemStock = floatval($systemStocks[$index] ?? 0);
|
|
$physicalStock = floatval($physicalStocks[$index] ?? 0);
|
|
$itemNote = $request->input("item_notes.{$index}");
|
|
|
|
// Jika ada perbedaan stock dan note kosong
|
|
if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) {
|
|
$product = Product::find($productId);
|
|
$stockDifferences[] = $product->name;
|
|
}
|
|
}
|
|
|
|
if (!empty($stockDifferences)) {
|
|
throw new \Exception(
|
|
'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' .
|
|
implode(', ', $stockDifferences)
|
|
);
|
|
}
|
|
}
|
|
|
|
// 8. Create Opname master record with approved status
|
|
$opname = Opname::create([
|
|
'dealer_id' => $dealerId,
|
|
'opname_date' => $opnameDate,
|
|
'user_id' => $userId,
|
|
'note' => $note,
|
|
'status' => OpnameStatus::APPROVED, // Set status langsung approved
|
|
'approved_by' => $userId, // Set current user sebagai approver
|
|
'approved_at' => now() // Set waktu approval
|
|
]);
|
|
|
|
// 9. Create OpnameDetails and update stock - only for valid entries
|
|
$details = [];
|
|
$processedCount = 0;
|
|
|
|
foreach ($productIds as $index => $productId) {
|
|
if (!$productId) continue;
|
|
|
|
// Skip only if physical stock is truly not provided (empty string or null)
|
|
// Accept 0 as valid input
|
|
if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) {
|
|
continue;
|
|
}
|
|
|
|
// Validate that physical stock is numeric (including 0)
|
|
if (!is_numeric($physicalStocks[$index])) {
|
|
continue;
|
|
}
|
|
|
|
$systemStock = floatval($systemStocks[$index] ?? 0);
|
|
$physicalStock = floatval($physicalStocks[$index]);
|
|
$difference = $physicalStock - $systemStock;
|
|
|
|
$processedCount++;
|
|
|
|
// Get item note (only for regular opname form)
|
|
$itemNote = null;
|
|
if (!$isTransactionForm) {
|
|
$itemNote = $request->input("item_notes.{$index}");
|
|
}
|
|
|
|
// Create opname detail
|
|
$details[] = [
|
|
'opname_id' => $opname->id,
|
|
'product_id' => $productId,
|
|
'system_stock' => $systemStock,
|
|
'physical_stock' => $physicalStock,
|
|
'difference' => $difference,
|
|
'note' => $itemNote,
|
|
'created_at' => now(),
|
|
'updated_at' => now()
|
|
];
|
|
|
|
// Update stock langsung karena auto approve
|
|
$stock = Stock::firstOrCreate(
|
|
[
|
|
'product_id' => $productId,
|
|
'dealer_id' => $dealerId
|
|
],
|
|
['quantity' => 0]
|
|
);
|
|
|
|
// Update stock dengan physical stock
|
|
$stock->updateStock(
|
|
$physicalStock,
|
|
$opname,
|
|
"Stock adjustment from auto-approved opname #{$opname->id}"
|
|
);
|
|
}
|
|
|
|
// Validate we have at least one detail to insert
|
|
if (empty($details)) {
|
|
throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.');
|
|
}
|
|
|
|
// Bulk insert untuk performa lebih baik
|
|
OpnameDetail::insert($details);
|
|
|
|
// 10. Log aktivitas dengan detail produk yang diproses
|
|
$processedProducts = collect($details)->map(function($detail) {
|
|
return [
|
|
'product_id' => $detail['product_id'],
|
|
'system_stock' => $detail['system_stock'],
|
|
'physical_stock' => $detail['physical_stock'],
|
|
'difference' => $detail['difference']
|
|
];
|
|
});
|
|
|
|
Log::info('Opname created and auto-approved', [
|
|
'opname_id' => $opname->id,
|
|
'dealer_id' => $opname->dealer_id,
|
|
'user_id' => $userId,
|
|
'approver_id' => $userId,
|
|
'product_count' => count($details),
|
|
'processed_count' => $processedCount,
|
|
'form_type' => $isTransactionForm ? 'transaction' : 'regular',
|
|
'opname_date' => $opnameDate->format('Y-m-d'),
|
|
'processed_products' => $processedProducts->toArray()
|
|
]);
|
|
|
|
DB::commit();
|
|
|
|
if ($isTransactionForm) {
|
|
// Redirect back to transaction page with success message and tab indicator
|
|
return redirect()
|
|
->route('transaction')
|
|
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.")
|
|
->with('active_tab', 'opname');
|
|
} else {
|
|
// Redirect to opname index for regular form
|
|
return redirect()
|
|
->route('opnames.index')
|
|
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
|
}
|
|
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
DB::rollBack();
|
|
Log::error('Validation error in OpnamesController@store', [
|
|
'errors' => $e->errors(),
|
|
'input' => $request->all()
|
|
]);
|
|
|
|
if ($isTransactionForm) {
|
|
return redirect()
|
|
->route('transaction')
|
|
->withErrors($e->validator)
|
|
->withInput()
|
|
->with('error', 'Terjadi kesalahan validasi. Periksa kembali data yang dimasukkan.')
|
|
->with('active_tab', 'opname');
|
|
} else {
|
|
return back()->withErrors($e->validator)->withInput();
|
|
}
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
|
Log::error($e->getTraceAsString());
|
|
Log::error('Request data:', $request->all());
|
|
|
|
$errorMessage = $e->getMessage();
|
|
|
|
if ($isTransactionForm) {
|
|
return redirect()
|
|
->route('transaction')
|
|
->with('error', $errorMessage)
|
|
->withInput()
|
|
->with('active_tab', 'opname');
|
|
} else {
|
|
return back()
|
|
->with('error', $errorMessage)
|
|
->withInput();
|
|
}
|
|
}
|
|
}
|
|
|
|
public function show(Request $request, $id)
|
|
{
|
|
try {
|
|
$opname = Opname::with('details.product', 'user')->findOrFail($id);
|
|
|
|
if ($request->ajax()) {
|
|
return DataTables::of($opname->details)
|
|
->addIndexColumn()
|
|
->addColumn('opname_date', function () use ($opname) {
|
|
return Carbon::parse($opname->opname_date)->format('d M Y');
|
|
})
|
|
->addColumn('user_name', function () use ($opname) {
|
|
return $opname->user ? $opname->user->name : '-';
|
|
})
|
|
->addColumn('product_name', function ($detail) {
|
|
return $detail->product->name ?? '-';
|
|
})
|
|
->addColumn('system_stock', function ($detail) {
|
|
return $detail->system_stock;
|
|
})
|
|
->addColumn('physical_stock', function ($detail) {
|
|
return $detail->physical_stock;
|
|
})
|
|
->addColumn('difference', function ($detail) {
|
|
return $detail->difference;
|
|
})
|
|
->make(true);
|
|
}
|
|
|
|
return view('warehouse_management.opnames.detail', compact('opname'));
|
|
} catch (\Exception $ex) {
|
|
Log::error($ex->getMessage());
|
|
abort(500, 'Something went wrong');
|
|
}
|
|
}
|
|
|
|
// Add new method to get stock data via AJAX
|
|
public function getStockData(Request $request)
|
|
{
|
|
try {
|
|
$dealerId = $request->dealer_id;
|
|
$productIds = $request->product_ids;
|
|
|
|
if (!$dealerId || !$productIds) {
|
|
return response()->json(['error' => 'Dealer ID dan Product IDs diperlukan'], 400);
|
|
}
|
|
|
|
$stocks = Stock::where('dealer_id', $dealerId)
|
|
->whereIn('product_id', $productIds)
|
|
->get()
|
|
->mapWithKeys(function ($stock) {
|
|
return [$stock->product_id => $stock->quantity];
|
|
});
|
|
|
|
return response()->json(['stocks' => $stocks]);
|
|
} catch (\Exception $e) {
|
|
Log::error('Error getting stock data: ' . $e->getMessage());
|
|
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
|
}
|
|
}
|
|
|
|
public function print($id)
|
|
{
|
|
try {
|
|
$opname = Opname::with(['details.product.category', 'user', 'dealer'])
|
|
->findOrFail($id);
|
|
|
|
return view('warehouse_management.opnames.print', compact('opname'));
|
|
} catch (\Exception $e) {
|
|
Log::error('Error printing opname: ' . $e->getMessage());
|
|
return back()->with('error', 'Gagal membuka halaman print opname.');
|
|
}
|
|
}
|
|
|
|
|
|
}
|