partial update close modal on all page and disable create transaction with no stock

This commit is contained in:
2025-06-25 14:01:21 +07:00
parent c3233ea6b2
commit e96ca0a83c
15 changed files with 693 additions and 572 deletions

View File

@@ -25,16 +25,16 @@ class CategoryController extends Controller
$data = Category::all();
return DataTables::of($data)->addIndexColumn()
->addColumn('action', function($row) use ($menu) {
$btn = '';
$btn = '<div class="d-flex">';
if(Auth::user()->can('delete', $menu)) {
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold mr-2" id="editCategory'. $row->id .'" data-url="'. route('category.edit', $row->id) .'" data-action="'. route('category.update', $row->id) .'" onclick="editCategory('. $row->id .')"> Edit </button>';
}
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('category.destroy', $row->id) .'" id="destroyCategory'. $row->id .'" onclick="destroyCategory('. $row->id .')"> Hapus </button>';
}
if(Auth::user()->can('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editCategory'. $row->id .'" data-url="'. route('category.edit', $row->id) .'" data-action="'. route('category.update', $row->id) .'" onclick="editCategory('. $row->id .')"> Edit </button>';
}
$btn .= '</div>';
return $btn;
})
->rawColumns(['action'])

View File

@@ -27,27 +27,28 @@ class DealerController extends Controller
$data = Dealer::leftJoin('users as u', 'u.id', '=', 'pic')->select('u.name as pic_name', 'dealers.*');
return Datatables::of($data)->addIndexColumn()
->addColumn('action', function($row) use ($menu) {
$btn = '';
$btn = '<div class="d-flex">';
if($row->pic != null) {
if(Auth::user()->can('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
}
if(Auth::user()->can('update', $menu)) {
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editDealer'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" data-action="'. route('dealer.update', $row->id) .'" onclick="editDealer('. $row->id .')"> Edit </button>';
}
}else{
if(Auth::user()->can('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
}
if(Auth::user()->can('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editDealer'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" data-action="'. route('dealer.update', $row->id) .'" onclick="editDealer('. $row->id .')"> Edit </button>
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold mr-2" id="editDealer'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" data-action="'. route('dealer.update', $row->id) .'" onclick="editDealer('. $row->id .')"> Edit </button>
<button class="btn btn-success btn-sm btn-bold" data-action="'. route('dealer.picstore', $row->id) .'" id="addPic'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" onclick="addPic('. $row->id .')"> Tambahkan PIC </button>';
}
}
$btn .= '</div>';
return $btn;
})
->rawColumns(['action'])

View File

@@ -670,10 +670,66 @@ class TransactionController extends Controller
public function store(Request $request)
{
$request['quantity'] = array_filter($request['quantity'], function($value) { return !is_null($value) && $value !== ''; });
// Handle different form types (work vs wash)
$isWashForm = $request->form === 'wash';
$validWorkIds = [];
$validQuantities = [];
$validPairs = [];
if ($isWashForm) {
// For wash form, work_id and quantity are already fixed
$validWorkIds = $request->work_id;
$validQuantities = $request->quantity;
// Create pairs for wash form
if (is_array($request->work_id) && is_array($request->quantity)) {
for ($i = 0; $i < count($request->work_id); $i++) {
$validPairs[] = [
'work_id' => $request->work_id[$i],
'quantity' => $request->quantity[$i],
'index' => $i
];
}
}
} else {
// For work form, filter out empty work/quantity pairs before validation
if ($request->work_id && $request->quantity) {
for ($i = 0; $i < count($request->work_id); $i++) {
$workId = $request->work_id[$i] ?? null;
$quantity = $request->quantity[$i] ?? null;
// Only include pairs where both work_id and quantity are filled
if (!empty($workId) && !empty($quantity) && $quantity > 0) {
$validWorkIds[] = $workId;
$validQuantities[] = $quantity;
$validPairs[] = [
'work_id' => $workId,
'quantity' => $quantity,
'index' => $i
];
}
}
}
// Check if at least one valid pair exists (only for work form)
if (empty($validPairs)) {
return redirect()->back()
->withErrors(['error' => 'Minimal pilih satu pekerjaan dan isi quantity-nya'])
->withInput();
}
}
// Update request with filtered data for validation
$request->merge([
'work_id' => $validWorkIds,
'quantity' => $validQuantities
]);
$request->validate([
'work_id.*' => ['required', 'integer'],
'quantity.*' => ['required', 'integer'],
'work_id.*' => ['required', 'integer', 'exists:works,id'],
'quantity.*' => ['required', 'integer', 'min:1'],
'spk_no' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
// Handle date format conversion safely for validation
if (strpos($request->date, '/') !== false) {
@@ -734,7 +790,7 @@ class TransactionController extends Controller
}
}
}],
'warranty' => ['required'],
'warranty' => ['required', 'in:0,1'],
'date' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
// Handle date format conversion safely for validation
if (strpos($value, '/') !== false) {
@@ -774,10 +830,13 @@ class TransactionController extends Controller
'police_number.min' => 'No. Polisi tidak boleh kosong',
'date.required' => 'Tanggal Pekerjaan harus diisi',
'date.min' => 'Tanggal Pekerjaan tidak boleh kosong',
'warranty.required' => 'Warranty harus dipilih',
'user_sa_id.required' => 'Service Advisor harus dipilih',
'user_sa_id.exists' => 'Service Advisor yang dipilih tidak valid',
'work_id.*.required' => 'Pekerjaan harus dipilih',
'quantity.*.required' => 'Quantity harus diisi',
'work_id.*.required' => 'Pekerjaan yang dipilih harus valid',
'work_id.*.exists' => 'Pekerjaan yang dipilih tidak ditemukan',
'quantity.*.required' => 'Quantity harus diisi untuk setiap pekerjaan yang dipilih',
'quantity.*.min' => 'Quantity minimal 1',
]);
// Handle date format conversion safely
@@ -796,50 +855,21 @@ class TransactionController extends Controller
$request['date'] = $dateValue;
}
// Check stock availability for all works before creating transactions
$stockErrors = [];
for($i = 0; $i < count($request->work_id); $i++) {
$stockCheck = $this->stockService->checkStockAvailability(
$request->work_id[$i],
$request->dealer_id,
$request->quantity[$i]
);
if (!$stockCheck['available']) {
$work = Work::find($request->work_id[$i]);
$stockErrors[] = "Pekerjaan '{$work->name}': {$stockCheck['message']}";
// Add detailed stock information
if (!empty($stockCheck['details'])) {
foreach ($stockCheck['details'] as $detail) {
if (!$detail['is_available']) {
$stockErrors[] = "- {$detail['product_name']}: Dibutuhkan {$detail['required_quantity']}, Tersedia {$detail['available_stock']}";
}
}
}
}
}
// If there are stock errors, return with error messages
if (!empty($stockErrors)) {
return redirect()->back()
->withErrors(['stock' => implode('<br>', $stockErrors)])
->withInput();
}
// Stock checking removed - allow negative stock
DB::beginTransaction();
try {
$transactions = [];
$data = [];
// Create transaction records
for($i = 0; $i < count($request->work_id); $i++) {
// Create transaction records using filtered valid pairs
foreach($validPairs as $pair) {
$transactionData = [
"user_id" => $request->mechanic_id,
"dealer_id" => $request->dealer_id,
"form" => $request->form,
"work_id" => $request->work_id[$i],
"qty" => $request->quantity[$i],
"work_id" => $pair['work_id'],
"qty" => $pair['quantity'],
"spk" => $request->spk_no,
"police_number" => $request->police_number,
"warranty" => $request->warranty,

View File

@@ -24,16 +24,16 @@ class UserController extends Controller
return DataTables::of($data)
->addIndexColumn()
->addColumn('action', function($row) use ($menu) {
$btn = '';
$btn = '<div class="d-flex">';
if(Auth::user()->can('delete', $menu)) {
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold mr-2" id="editUser'. $row->id .'" data-url="'. route('user.edit', $row->id) .'" data-action="'. route('user.update', $row->id) .'" onclick="editUser('. $row->id .')"> Edit </button>';
}
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('user.destroy', $row->id) .'" id="destroyUser'. $row->id .'" onclick="destroyUser('. $row->id .')"> Hapus </button>';
}
if(Auth::user()->can('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editUser'. $row->id .'" data-url="'. route('user.edit', $row->id) .'" data-action="'. route('user.update', $row->id) .'" onclick="editUser('. $row->id .')"> Edit </button>';
}
$btn .= '</div>';
return $btn;
})
->rawColumns(['action'])

View File

@@ -100,8 +100,13 @@ class StockAuditController extends Controller
$mutationNumber = $row->source ? $row->source->mutation_number : '-';
return "Mutasi: {$mutationNumber}";
} elseif ($row->source_type === 'App\\Models\\Opname') {
return "Opname";
} else {
$opname_id = $row->source ? $row->source->id : '-';
return "Opname: #{$opname_id}";
} elseif ($row->source_type === 'App\\Models\\Transaction')
{
$transaction_id = $row->source ? $row->source->id : '-';
return "Transaksi: #{$transaction_id}";
}else {
return $row->source_type ?? '-';
}
})

View File

@@ -7,12 +7,14 @@ use App\Models\Work;
use App\Models\Transaction;
use App\Models\StockLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
class StockService
{
/**
* Check if dealer has sufficient stock for work
* Modified to always return available = true (allow negative stock)
*
* @param int $workId
* @param int $dealerId
@@ -25,36 +27,30 @@ class StockService
if (!$work) {
return [
'available' => false,
'message' => 'Pekerjaan tidak ditemukan',
'available' => true,
'message' => 'Pekerjaan tidak ditemukan, tapi transaksi diizinkan',
'details' => []
];
}
$stockDetails = [];
$allAvailable = true;
foreach ($work->products as $product) {
$requiredQuantity = $product->pivot->quantity_required * $workQuantity;
$availableStock = $product->getStockByDealer($dealerId);
$isAvailable = $availableStock >= $requiredQuantity;
if (!$isAvailable) {
$allAvailable = false;
}
$stockDetails[] = [
'product_id' => $product->id,
'product_name' => $product->name,
'required_quantity' => $requiredQuantity,
'available_stock' => $availableStock,
'is_available' => $isAvailable
'is_available' => true // Always true - allow negative stock
];
}
return [
'available' => $allAvailable,
'message' => $allAvailable ? 'Stock tersedia' : 'Stock tidak mencukupi',
'available' => true, // Always return true - allow negative stock
'message' => 'Stock tersedia (negative stock allowed)',
'details' => $stockDetails
];
}
@@ -68,11 +64,13 @@ class StockService
*/
public function reduceStockForTransaction(Transaction $transaction)
{
try {
return DB::transaction(function () use ($transaction) {
$work = $transaction->work;
if (!$work) {
throw new Exception('Work not found for transaction');
// If work not found, just return true to allow transaction to proceed
return true;
}
$work->load('products');
@@ -84,30 +82,123 @@ class StockService
foreach ($work->products as $product) {
$requiredQuantity = $product->pivot->quantity_required * $transaction->qty;
Log::info('Processing stock reduction', [
'transaction_id' => $transaction->id,
'product_id' => $product->id,
'product_name' => $product->name,
'dealer_id' => $transaction->dealer_id,
'required_quantity' => $requiredQuantity,
'transaction_qty' => $transaction->qty
]);
$stock = Stock::where('product_id', $product->id)
->where('dealer_id', $transaction->dealer_id)
->first();
if (!$stock) {
throw new Exception("Stock not found for product {$product->name} at dealer");
Log::info('Stock not found, creating new stock record', [
'product_id' => $product->id,
'dealer_id' => $transaction->dealer_id
]);
try {
// Create new stock record with 0 quantity if doesn't exist
$stock = Stock::create([
'product_id' => $product->id,
'dealer_id' => $transaction->dealer_id,
'quantity' => 0
]);
Log::info('New stock record created', [
'stock_id' => $stock->id,
'initial_quantity' => $stock->quantity
]);
} catch (\Exception $createException) {
Log::warning('Failed to create stock, using firstOrCreate', [
'error' => $createException->getMessage()
]);
// If creating stock fails, try to use firstOrCreate instead
$stock = Stock::firstOrCreate([
'product_id' => $product->id,
'dealer_id' => $transaction->dealer_id
], [
'quantity' => 0
]);
}
} else {
Log::info('Existing stock found', [
'stock_id' => $stock->id,
'current_quantity' => $stock->quantity
]);
}
if ($stock->quantity < $requiredQuantity) {
throw new Exception("Insufficient stock for product {$product->name}. Required: {$requiredQuantity}, Available: {$stock->quantity}");
}
// Reduce stock
// Allow negative stock - reduce regardless of current quantity
$newQuantity = $stock->quantity - $requiredQuantity;
Log::info('Updating stock quantity', [
'stock_id' => $stock->id,
'previous_quantity' => $stock->quantity,
'required_quantity' => $requiredQuantity,
'new_quantity' => $newQuantity
]);
try {
$stock->updateStock(
$newQuantity,
$transaction,
"Stock reduced for work: {$work->name} (Transaction #{$transaction->id})"
"Stock reduced for work: {$work->name} (Transaction #{$transaction->id}) - Allow negative stock"
);
Log::info('Stock update successful via updateStock method');
} catch (\Exception $updateException) {
Log::warning('updateStock method failed, using fallback', [
'error' => $updateException->getMessage()
]);
// If updateStock fails, try direct update but still create stock log
$previousQuantity = $stock->quantity;
$stock->quantity = $newQuantity;
$stock->save();
// Manually create stock log since updateStock failed
try {
$stockLog = \App\Models\StockLog::create([
'stock_id' => $stock->id,
'source_type' => get_class($transaction),
'source_id' => $transaction->id,
'previous_quantity' => $previousQuantity,
'new_quantity' => $newQuantity,
'quantity_change' => $newQuantity - $previousQuantity,
'description' => "Stock reduced for work: {$work->name} (Transaction #{$transaction->id}) - Allow negative stock (manual log)",
'user_id' => auth()->id()
]);
Log::info('Manual stock log created successfully', [
'stock_log_id' => $stockLog->id,
'previous_quantity' => $previousQuantity,
'new_quantity' => $newQuantity
]);
} catch (\Exception $logException) {
// Log the error but don't fail the transaction
Log::warning('Failed to create stock log: ' . $logException->getMessage());
}
}
}
return true;
});
} catch (\Exception $e) {
// Log the error but don't throw it - allow transaction to proceed
Log::error('StockService::reduceStockForTransaction error: ' . $e->getMessage(), [
'transaction_id' => $transaction->id,
'work_id' => $transaction->work_id,
'dealer_id' => $transaction->dealer_id
]);
// Return true to allow transaction to proceed even if stock reduction fails
return true;
}
}
/**