partial update transaction work with stock product
This commit is contained in:
@@ -9,17 +9,30 @@ use App\Models\Stock;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Models\Work;
|
||||
use App\Services\StockService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Exception;
|
||||
|
||||
class TransactionController extends Controller
|
||||
{
|
||||
protected $stockService;
|
||||
|
||||
public function __construct(StockService $stockService)
|
||||
{
|
||||
$this->stockService = $stockService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$work_works = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')->select('c.name as category_name', 'works.*')->where('c.name', 'LIKE', '%kerja%')->get();
|
||||
$work_works = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')
|
||||
->select('c.name as category_name', 'works.*')
|
||||
->where('c.name', 'LIKE', '%kerja%')
|
||||
->orderBy('works.name', 'asc')
|
||||
->get();
|
||||
$wash_work = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')->select('c.name as category_name', 'works.*')->where('c.name', 'LIKE', '%cuci%')->first();
|
||||
$user_sas = User::where('role_id', 4)->where('dealer_id', Auth::user()->dealer_id)->get();
|
||||
$count_transaction_users = Transaction::where("user_id", Auth::user()->id)->count();
|
||||
@@ -41,7 +54,9 @@ class TransactionController extends Controller
|
||||
|
||||
public function workcategory($category_id)
|
||||
{
|
||||
$works = Work::where('category_id', $category_id)->get();
|
||||
$works = Work::where('category_id', $category_id)
|
||||
->orderBy('name', 'asc')
|
||||
->get();
|
||||
$response = [
|
||||
"message" => "get work category successfully",
|
||||
"data" => $works,
|
||||
@@ -629,14 +644,28 @@ class TransactionController extends Controller
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
Transaction::find($id)->delete();
|
||||
|
||||
$response = [
|
||||
'message' => 'Data deleted successfully',
|
||||
'status' => 200
|
||||
];
|
||||
|
||||
return redirect()->back();
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$transaction = Transaction::find($id);
|
||||
|
||||
if (!$transaction) {
|
||||
return redirect()->back()->withErrors(['error' => 'Transaksi tidak ditemukan']);
|
||||
}
|
||||
|
||||
// Restore stock before deleting transaction
|
||||
$this->stockService->restoreStockForTransaction($transaction);
|
||||
|
||||
// Delete the transaction
|
||||
$transaction->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->back()->with('success', 'Transaksi berhasil dihapus dan stock telah dikembalikan');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
return redirect()->back()->withErrors(['error' => 'Gagal menghapus transaksi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -645,9 +674,19 @@ class TransactionController extends Controller
|
||||
$request->validate([
|
||||
'work_id.*' => ['required', 'integer'],
|
||||
'quantity.*' => ['required', 'integer'],
|
||||
'spk_no' => ['required', function($attribute, $value, $fail) use($request) {
|
||||
$date = explode('/', $request->date);
|
||||
$date = $date[2].'-'.$date[0].'-'.$date[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) {
|
||||
$dateParts = explode('/', $request->date);
|
||||
if (count($dateParts) === 3) {
|
||||
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
$fail('Format tanggal tidak valid');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$date = $request->date;
|
||||
}
|
||||
|
||||
if(!$request->work_id) {
|
||||
$fail('Pekerjaan harus diisi');
|
||||
@@ -665,9 +704,19 @@ class TransactionController extends Controller
|
||||
}
|
||||
}
|
||||
}],
|
||||
'police_number' => ['required', function($attribute, $value, $fail) use($request) {
|
||||
$date = explode('/', $request->date);
|
||||
$date = $date[2].'-'.$date[0].'-'.$date[1];
|
||||
'police_number' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
|
||||
// Handle date format conversion safely for validation
|
||||
if (strpos($request->date, '/') !== false) {
|
||||
$dateParts = explode('/', $request->date);
|
||||
if (count($dateParts) === 3) {
|
||||
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
$fail('Format tanggal tidak valid');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$date = $request->date;
|
||||
}
|
||||
|
||||
if(!$request->work_id) {
|
||||
$fail('Pekerjaan harus diisi');
|
||||
@@ -686,9 +735,19 @@ class TransactionController extends Controller
|
||||
}
|
||||
}],
|
||||
'warranty' => ['required'],
|
||||
'date' => ['required', function($attribute, $value, $fail) use($request) {
|
||||
$date = explode('/', $value);
|
||||
$date = $date[2].'-'.$date[0].'-'.$date[1];
|
||||
'date' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
|
||||
// Handle date format conversion safely for validation
|
||||
if (strpos($value, '/') !== false) {
|
||||
$dateParts = explode('/', $value);
|
||||
if (count($dateParts) === 3) {
|
||||
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
$fail('Format tanggal tidak valid. Gunakan format MM/DD/YYYY atau YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$date = $value;
|
||||
}
|
||||
|
||||
if(!$request->work_id) {
|
||||
$fail('Pekerjaan harus diisi');
|
||||
@@ -707,31 +766,117 @@ class TransactionController extends Controller
|
||||
}
|
||||
}],
|
||||
'category' => ['required'],
|
||||
'user_sa_id' => ['required', 'integer'],
|
||||
'user_sa_id' => ['required', 'integer', 'exists:users,id'],
|
||||
], [
|
||||
'spk_no.required' => 'No. SPK harus diisi',
|
||||
'spk_no.min' => 'No. SPK tidak boleh kosong',
|
||||
'police_number.required' => 'No. Polisi harus diisi',
|
||||
'police_number.min' => 'No. Polisi tidak boleh kosong',
|
||||
'date.required' => 'Tanggal Pekerjaan harus diisi',
|
||||
'date.min' => 'Tanggal Pekerjaan tidak boleh kosong',
|
||||
'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',
|
||||
]);
|
||||
|
||||
$request['date'] = explode('/', $request->date);
|
||||
$request['date'] = $request['date'][2].'-'.$request['date'][0].'-'.$request['date'][1];
|
||||
|
||||
$data = [];
|
||||
for($i = 0; $i < count($request->work_id); $i++) {
|
||||
$data[] = [
|
||||
"user_id" => $request->mechanic_id,
|
||||
"dealer_id" => $request->dealer_id,
|
||||
"form" => $request->form,
|
||||
"work_id" => $request->work_id[$i],
|
||||
"qty" => $request->quantity[$i],
|
||||
"spk" => $request->spk_no,
|
||||
"police_number" => $request->police_number,
|
||||
"warranty" => $request->warranty,
|
||||
"user_sa_id" => $request->user_sa_id,
|
||||
"date" => $request->date,
|
||||
"created_at" => date('Y-m-d H:i:s')
|
||||
];
|
||||
// Handle date format conversion safely
|
||||
$dateValue = $request->date;
|
||||
if (strpos($dateValue, '/') !== false) {
|
||||
// If date is in MM/DD/YYYY format, convert to Y-m-d
|
||||
$dateParts = explode('/', $dateValue);
|
||||
if (count($dateParts) === 3) {
|
||||
$request['date'] = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
// Invalid date format, use as is
|
||||
$request['date'] = $dateValue;
|
||||
}
|
||||
} else {
|
||||
// Date is already in Y-m-d format or other format, use as is
|
||||
$request['date'] = $dateValue;
|
||||
}
|
||||
|
||||
Transaction::insert($data);
|
||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan');
|
||||
// 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();
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$transactions = [];
|
||||
$data = [];
|
||||
|
||||
// Create transaction records
|
||||
for($i = 0; $i < count($request->work_id); $i++) {
|
||||
$transactionData = [
|
||||
"user_id" => $request->mechanic_id,
|
||||
"dealer_id" => $request->dealer_id,
|
||||
"form" => $request->form,
|
||||
"work_id" => $request->work_id[$i],
|
||||
"qty" => $request->quantity[$i],
|
||||
"spk" => $request->spk_no,
|
||||
"police_number" => $request->police_number,
|
||||
"warranty" => $request->warranty,
|
||||
"user_sa_id" => $request->user_sa_id,
|
||||
"date" => $request->date,
|
||||
"status" => 'completed', // Mark as completed to trigger stock reduction
|
||||
"created_at" => date('Y-m-d H:i:s'),
|
||||
"updated_at" => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$data[] = $transactionData;
|
||||
}
|
||||
|
||||
// Insert all transactions
|
||||
Transaction::insert($data);
|
||||
|
||||
// Get the created transactions for stock reduction
|
||||
$createdTransactions = Transaction::where('spk', $request->spk_no)
|
||||
->where('police_number', $request->police_number)
|
||||
->where('date', $request->date)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->get();
|
||||
|
||||
// Reduce stock for each transaction
|
||||
foreach ($createdTransactions as $transaction) {
|
||||
$this->stockService->reduceStockForTransaction($transaction);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
return redirect()->back()
|
||||
->withErrors(['error' => 'Gagal menyimpan transaksi: ' . $e->getMessage()])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
@@ -764,4 +909,62 @@ class TransactionController extends Controller
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability for work at dealer
|
||||
*/
|
||||
public function checkStockAvailability(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
try {
|
||||
$availability = $this->stockService->checkStockAvailability(
|
||||
$request->work_id,
|
||||
$request->dealer_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $availability
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Error checking stock: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock prediction for work
|
||||
*/
|
||||
public function getStockPrediction(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
try {
|
||||
$prediction = $this->stockService->getStockUsagePrediction(
|
||||
$request->work_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $prediction
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Error getting prediction: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,16 +26,28 @@ class WorkController extends Controller
|
||||
$data = DB::table('works as w')->leftJoin('categories as c', 'c.id', '=', 'w.category_id')->select('w.shortname as shortname', 'w.id as work_id', 'w.name as name', 'w.desc as desc', 'c.name as category_name', 'w.category_id as category_id');
|
||||
return DataTables::of($data)->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')"> Hapus </button>';
|
||||
// Products Management Button
|
||||
if(Gate::allows('view', $menu)) {
|
||||
$btn .= '<a href="'. route('work.products.index', ['work' => $row->work_id]) .'" class="btn btn-info btn-sm" title="Kelola Produk">
|
||||
Produk
|
||||
</a>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')"> Edit </button>';
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
|
||||
Edit
|
||||
</button>';
|
||||
}
|
||||
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')">
|
||||
Hapus
|
||||
</button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
|
||||
247
app/Http/Controllers/WorkProductController.php
Normal file
247
app/Http/Controllers/WorkProductController.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Work;
|
||||
use App\Models\Product;
|
||||
use App\Models\WorkProduct;
|
||||
use App\Models\Menu;
|
||||
use App\Services\StockService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\DataTables;
|
||||
|
||||
class WorkProductController extends Controller
|
||||
{
|
||||
protected $stockService;
|
||||
|
||||
public function __construct(StockService $stockService)
|
||||
{
|
||||
$this->stockService = $stockService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display work products for a specific work
|
||||
*/
|
||||
public function index(Request $request, $workId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$work = Work::with('category')->findOrFail($workId);
|
||||
|
||||
if ($request->ajax()) {
|
||||
Log::info('Work products index AJAX request for work ID: ' . $workId);
|
||||
|
||||
$workProducts = WorkProduct::with(['product', 'product.category'])
|
||||
->where('work_id', $workId)
|
||||
->get();
|
||||
|
||||
Log::info('Found ' . $workProducts->count() . ' work products');
|
||||
|
||||
return DataTables::of($workProducts)
|
||||
->addIndexColumn()
|
||||
->addColumn('product_name', function($row) {
|
||||
return $row->product->name;
|
||||
})
|
||||
->addColumn('product_code', function($row) {
|
||||
return $row->product->code;
|
||||
})
|
||||
->addColumn('product_category', function($row) {
|
||||
return $row->product->category ? $row->product->category->name : '-';
|
||||
})
|
||||
->addColumn('unit', function($row) {
|
||||
return $row->product->unit;
|
||||
})
|
||||
->addColumn('quantity_required', function($row) {
|
||||
return number_format($row->quantity_required, 2);
|
||||
})
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-edit-work-product" data-id="'.$row->id.'">
|
||||
Edit
|
||||
</button>';
|
||||
}
|
||||
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-delete-work-product" data-id="'.$row->id.'">
|
||||
Hapus
|
||||
</button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
$products = Product::where('active', true)->with('category')->get();
|
||||
|
||||
return view('back.master.work-products', compact('work', 'products'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store work product relationship
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('create', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'quantity_required' => 'required|numeric|min:0.01',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
// Check if combination already exists
|
||||
$exists = WorkProduct::where('work_id', $request->work_id)
|
||||
->where('product_id', $request->product_id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Produk sudah ditambahkan ke pekerjaan ini'
|
||||
], 422);
|
||||
}
|
||||
|
||||
WorkProduct::create($request->all());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Produk berhasil ditambahkan ke pekerjaan'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show work product for editing
|
||||
*/
|
||||
public function show($workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::with(['work', 'product', 'product.category'])
|
||||
->where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $workProduct
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update work product relationship
|
||||
*/
|
||||
public function update(Request $request, $workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('update', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'quantity_required' => 'required|numeric|min:0.01',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
$workProduct->update($request->only(['quantity_required', 'notes']));
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Data produk pekerjaan berhasil diupdate'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove work product relationship
|
||||
*/
|
||||
public function destroy($workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('delete', $menu), 403, 'Unauthorized User');
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
$workProduct->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Produk berhasil dihapus dari pekerjaan'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock prediction for work
|
||||
*/
|
||||
public function stockPrediction(Request $request, $workId)
|
||||
{
|
||||
$quantity = $request->get('quantity', 1);
|
||||
$prediction = $this->stockService->getStockUsagePrediction($workId, $quantity);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $prediction
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability for work at specific dealer
|
||||
*/
|
||||
public function checkStock(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$availability = $this->stockService->checkStockAvailability(
|
||||
$request->work_id,
|
||||
$request->dealer_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $availability
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,26 @@ class Product extends Model
|
||||
{
|
||||
return $this->stocks()->where('dealer_id', $dealerId)->first()?->quantity ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all works that require this product
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function works()
|
||||
{
|
||||
return $this->belongsToMany(Work::class, 'work_products')
|
||||
->withPivot('quantity_required', 'notes')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get work products pivot records
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workProducts()
|
||||
{
|
||||
return $this->hasMany(WorkProduct::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,40 @@ class Transaction extends Model
|
||||
/**
|
||||
* Get the work associated with the Transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function work()
|
||||
{
|
||||
return $this->hasOne(Work::class, 'id', 'work_id');
|
||||
return $this->belongsTo(Work::class, 'work_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dealer associated with the Transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SA user associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function userSa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_sa_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,36 @@ class Work extends Model
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'work_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all products required for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'work_products')
|
||||
->withPivot('quantity_required', 'notes')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get work products pivot records
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workProducts()
|
||||
{
|
||||
return $this->hasMany(WorkProduct::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category associated with the Work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Models/WorkProduct.php
Normal file
32
app/Models/WorkProduct.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkProduct extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'work_id',
|
||||
'product_id',
|
||||
'quantity_required',
|
||||
'notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity_required' => 'decimal:2'
|
||||
];
|
||||
|
||||
public function work()
|
||||
{
|
||||
return $this->belongsTo(Work::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(\App\Services\StockService::class, function ($app) {
|
||||
return new \App\Services\StockService();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
197
app/Services/StockService.php
Normal file
197
app/Services/StockService.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Stock;
|
||||
use App\Models\Work;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\StockLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Exception;
|
||||
|
||||
class StockService
|
||||
{
|
||||
/**
|
||||
* Check if dealer has sufficient stock for work
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $dealerId
|
||||
* @param int $workQuantity
|
||||
* @return array
|
||||
*/
|
||||
public function checkStockAvailability($workId, $dealerId, $workQuantity = 1)
|
||||
{
|
||||
$work = Work::with('products')->find($workId);
|
||||
|
||||
if (!$work) {
|
||||
return [
|
||||
'available' => false,
|
||||
'message' => 'Pekerjaan tidak ditemukan',
|
||||
'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
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'available' => $allAvailable,
|
||||
'message' => $allAvailable ? 'Stock tersedia' : 'Stock tidak mencukupi',
|
||||
'details' => $stockDetails
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce stock when work transaction is completed
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function reduceStockForTransaction(Transaction $transaction)
|
||||
{
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
$work = $transaction->work;
|
||||
|
||||
if (!$work) {
|
||||
throw new Exception('Work not found for transaction');
|
||||
}
|
||||
|
||||
$work->load('products');
|
||||
|
||||
if ($work->products->isEmpty()) {
|
||||
// No products required for this work, return true
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$requiredQuantity = $product->pivot->quantity_required * $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");
|
||||
}
|
||||
|
||||
if ($stock->quantity < $requiredQuantity) {
|
||||
throw new Exception("Insufficient stock for product {$product->name}. Required: {$requiredQuantity}, Available: {$stock->quantity}");
|
||||
}
|
||||
|
||||
// Reduce stock
|
||||
$newQuantity = $stock->quantity - $requiredQuantity;
|
||||
$stock->updateStock(
|
||||
$newQuantity,
|
||||
$transaction,
|
||||
"Stock reduced for work: {$work->name} (Transaction #{$transaction->id})"
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore stock when work transaction is cancelled/reversed
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function restoreStockForTransaction(Transaction $transaction)
|
||||
{
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
$work = $transaction->work;
|
||||
|
||||
if (!$work) {
|
||||
throw new Exception('Work not found for transaction');
|
||||
}
|
||||
|
||||
$work->load('products');
|
||||
|
||||
if ($work->products->isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$restoreQuantity = $product->pivot->quantity_required * $transaction->qty;
|
||||
|
||||
$stock = Stock::where('product_id', $product->id)
|
||||
->where('dealer_id', $transaction->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$stock) {
|
||||
// Create new stock record if doesn't exist
|
||||
$stock = Stock::create([
|
||||
'product_id' => $product->id,
|
||||
'dealer_id' => $transaction->dealer_id,
|
||||
'quantity' => 0
|
||||
]);
|
||||
}
|
||||
|
||||
// Restore stock
|
||||
$newQuantity = $stock->quantity + $restoreQuantity;
|
||||
$stock->updateStock(
|
||||
$newQuantity,
|
||||
$transaction,
|
||||
"Stock restored from cancelled work: {$work->name} (Transaction #{$transaction->id})"
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock usage prediction for a work
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $quantity
|
||||
* @return array
|
||||
*/
|
||||
public function getStockUsagePrediction($workId, $quantity = 1)
|
||||
{
|
||||
$work = Work::with('products')->find($workId);
|
||||
|
||||
if (!$work) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$predictions = [];
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$totalRequired = $product->pivot->quantity_required * $quantity;
|
||||
|
||||
$predictions[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'product_code' => $product->code,
|
||||
'unit' => $product->unit,
|
||||
'quantity_per_work' => $product->pivot->quantity_required,
|
||||
'total_quantity_needed' => $totalRequired,
|
||||
'notes' => $product->pivot->notes
|
||||
];
|
||||
}
|
||||
|
||||
return $predictions;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user