partial update transaction work with stock product

This commit is contained in:
2025-06-24 19:42:19 +07:00
parent 33502e905d
commit c3233ea6b2
20 changed files with 3432 additions and 239 deletions

View File

@@ -9,17 +9,30 @@ use App\Models\Stock;
use App\Models\Transaction; use App\Models\Transaction;
use App\Models\User; use App\Models\User;
use App\Models\Work; use App\Models\Work;
use App\Services\StockService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Exception;
class TransactionController extends Controller class TransactionController extends Controller
{ {
protected $stockService;
public function __construct(StockService $stockService)
{
$this->stockService = $stockService;
}
public function index() 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(); $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(); $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(); $count_transaction_users = Transaction::where("user_id", Auth::user()->id)->count();
@@ -41,7 +54,9 @@ class TransactionController extends Controller
public function workcategory($category_id) 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 = [ $response = [
"message" => "get work category successfully", "message" => "get work category successfully",
"data" => $works, "data" => $works,
@@ -629,14 +644,28 @@ class TransactionController extends Controller
public function destroy($id) public function destroy($id)
{ {
Transaction::find($id)->delete(); DB::beginTransaction();
try {
$response = [ $transaction = Transaction::find($id);
'message' => 'Data deleted successfully',
'status' => 200 if (!$transaction) {
]; return redirect()->back()->withErrors(['error' => 'Transaksi tidak ditemukan']);
}
return redirect()->back();
// 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) public function store(Request $request)
@@ -645,9 +674,19 @@ class TransactionController extends Controller
$request->validate([ $request->validate([
'work_id.*' => ['required', 'integer'], 'work_id.*' => ['required', 'integer'],
'quantity.*' => ['required', 'integer'], 'quantity.*' => ['required', 'integer'],
'spk_no' => ['required', function($attribute, $value, $fail) use($request) { 'spk_no' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
$date = explode('/', $request->date); // Handle date format conversion safely for validation
$date = $date[2].'-'.$date[0].'-'.$date[1]; 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) { if(!$request->work_id) {
$fail('Pekerjaan harus diisi'); $fail('Pekerjaan harus diisi');
@@ -665,9 +704,19 @@ class TransactionController extends Controller
} }
} }
}], }],
'police_number' => ['required', function($attribute, $value, $fail) use($request) { 'police_number' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
$date = explode('/', $request->date); // Handle date format conversion safely for validation
$date = $date[2].'-'.$date[0].'-'.$date[1]; 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) { if(!$request->work_id) {
$fail('Pekerjaan harus diisi'); $fail('Pekerjaan harus diisi');
@@ -686,9 +735,19 @@ class TransactionController extends Controller
} }
}], }],
'warranty' => ['required'], 'warranty' => ['required'],
'date' => ['required', function($attribute, $value, $fail) use($request) { 'date' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
$date = explode('/', $value); // Handle date format conversion safely for validation
$date = $date[2].'-'.$date[0].'-'.$date[1]; 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) { if(!$request->work_id) {
$fail('Pekerjaan harus diisi'); $fail('Pekerjaan harus diisi');
@@ -707,31 +766,117 @@ class TransactionController extends Controller
} }
}], }],
'category' => ['required'], '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); // Handle date format conversion safely
$request['date'] = $request['date'][2].'-'.$request['date'][0].'-'.$request['date'][1]; $dateValue = $request->date;
if (strpos($dateValue, '/') !== false) {
$data = []; // If date is in MM/DD/YYYY format, convert to Y-m-d
for($i = 0; $i < count($request->work_id); $i++) { $dateParts = explode('/', $dateValue);
$data[] = [ if (count($dateParts) === 3) {
"user_id" => $request->mechanic_id, $request['date'] = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
"dealer_id" => $request->dealer_id, } else {
"form" => $request->form, // Invalid date format, use as is
"work_id" => $request->work_id[$i], $request['date'] = $dateValue;
"qty" => $request->quantity[$i], }
"spk" => $request->spk_no, } else {
"police_number" => $request->police_number, // Date is already in Y-m-d format or other format, use as is
"warranty" => $request->warranty, $request['date'] = $dateValue;
"user_sa_id" => $request->user_sa_id,
"date" => $request->date,
"created_at" => date('Y-m-d H:i:s')
];
} }
Transaction::insert($data); // Check stock availability for all works before creating transactions
return redirect()->back()->with('success', 'Berhasil input pekerjaan'); $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) public function edit($id)
@@ -764,4 +909,62 @@ class TransactionController extends Controller
return response()->json($response); 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);
}
}
} }

View File

@@ -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'); $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() return DataTables::of($data)->addIndexColumn()
->addColumn('action', function($row) use ($menu) { ->addColumn('action', function($row) use ($menu) {
$btn = ''; $btn = '<div class="d-flex flex-row gap-1">';
if(Auth::user()->can('delete', $menu)) { // Products Management Button
$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>'; 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)) { if(Gate::allows('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>'; $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; return $btn;
}) })
->rawColumns(['action']) ->rawColumns(['action'])

View 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
]);
}
}

View File

@@ -47,4 +47,26 @@ class Product extends Model
{ {
return $this->stocks()->where('dealer_id', $dealerId)->first()?->quantity ?? 0; 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);
}
} }

View File

@@ -16,10 +16,40 @@ class Transaction extends Model
/** /**
* Get the work associated with the Transaction * Get the work associated with the Transaction
* *
* @return \Illuminate\Database\Eloquent\Relations\HasOne * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
public function work() 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');
} }
} }

View File

@@ -22,4 +22,36 @@ class Work extends Model
{ {
return $this->hasMany(Transaction::class, 'work_id', 'id'); 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);
}
} }

View 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);
}
}

View File

@@ -17,7 +17,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register() public function register()
{ {
// $this->app->singleton(\App\Services\StockService::class, function ($app) {
return new \App\Services\StockService();
});
} }
/** /**

View 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;
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWorkProductsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('work_products', function (Blueprint $table) {
$table->id();
$table->foreignId('work_id')->constrained('works')->onDelete('cascade');
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
$table->decimal('quantity_required', 10, 2)->default(1);
$table->text('notes')->nullable();
$table->timestamps();
// Prevent duplicate work-product combinations
$table->unique(['work_id', 'product_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('work_products');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Database\Seeders;
use App\Models\Work;
use App\Models\Product;
use App\Models\WorkProduct;
use Illuminate\Database\Seeder;
class WorkProductSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Get some sample works and products
$works = Work::take(3)->get();
$products = Product::where('active', true)->take(5)->get();
if ($works->isEmpty() || $products->isEmpty()) {
$this->command->info('Tidak ada data Work atau Product untuk seeding. Silakan buat data tersebut terlebih dahulu.');
return;
}
// Sample work-product relationships
$workProductData = [
[
'work_id' => $works->first()->id,
'product_id' => $products->first()->id,
'quantity_required' => 2.0,
'notes' => 'Digunakan untuk pembersihan awal'
],
[
'work_id' => $works->first()->id,
'product_id' => $products->skip(1)->first()->id,
'quantity_required' => 1.0,
'notes' => 'Untuk finishing'
],
[
'work_id' => $works->skip(1)->first()->id,
'product_id' => $products->skip(2)->first()->id,
'quantity_required' => 3.0,
'notes' => 'Komponen utama'
],
[
'work_id' => $works->skip(1)->first()->id,
'product_id' => $products->skip(3)->first()->id,
'quantity_required' => 0.5,
'notes' => 'Pelumas tambahan'
],
[
'work_id' => $works->skip(2)->first()->id,
'product_id' => $products->skip(4)->first()->id,
'quantity_required' => 1.0,
'notes' => 'Standard usage'
]
];
foreach ($workProductData as $data) {
WorkProduct::firstOrCreate(
[
'work_id' => $data['work_id'],
'product_id' => $data['product_id']
],
[
'quantity_required' => $data['quantity_required'],
'notes' => $data['notes']
]
);
}
$this->command->info('Work Product relationships seeded successfully!');
}
}

View File

@@ -0,0 +1,297 @@
# Work Products & Stock Management System
## Overview
Sistem ini memungkinkan setiap pekerjaan (work) memiliki relasi dengan banyak produk (products) dan otomatis mengurangi stock di dealer ketika transaksi pekerjaan dilakukan.
## Fitur Utama
### 1. Work Products Management
- Setiap pekerjaan dapat dikonfigurasi untuk memerlukan produk tertentu
- Admin dapat mengatur jumlah (quantity) produk yang dibutuhkan per pekerjaan
- Mendukung catatan/notes untuk setiap produk
### 2. Automatic Stock Reduction
- Stock otomatis dikurangi ketika transaksi pekerjaan dibuat
- Validasi stock tersedia sebelum transaksi disimpan
- Stock dikembalikan ketika transaksi dihapus
### 3. Stock Validation & Warning
- Real-time checking stock availability saat memilih pekerjaan
- Warning ketika stock tidak mencukupi
- Konfirmasi user sebelum melanjutkan dengan stock negatif
### 4. Stock Prediction
- Melihat prediksi penggunaan stock untuk pekerjaan tertentu
- Kalkulasi berdasarkan quantity pekerjaan yang akan dilakukan
## Database Schema
### Tabel `work_products`
```sql
CREATE TABLE work_products (
id BIGINT PRIMARY KEY,
work_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity_required DECIMAL(10,2) DEFAULT 1.00,
notes TEXT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE KEY unique_work_product (work_id, product_id),
FOREIGN KEY (work_id) REFERENCES works(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
```
### Model Relationships
#### Work Model
```php
// Relasi many-to-many dengan Product
public function products()
{
return $this->belongsToMany(Product::class, 'work_products')
->withPivot('quantity_required', 'notes')
->withTimestamps();
}
// Relasi one-to-many dengan WorkProduct
public function workProducts()
{
return $this->hasMany(WorkProduct::class);
}
```
#### Product Model
```php
// Relasi many-to-many dengan Work
public function works()
{
return $this->belongsToMany(Work::class, 'work_products')
->withPivot('quantity_required', 'notes')
->withTimestamps();
}
```
## API Endpoints
### Work Products Management
```
GET /admin/work/{work}/products - List work products
POST /admin/work/{work}/products - Add product to work
GET /admin/work/{work}/products/{id} - Show work product
PUT /admin/work/{work}/products/{id} - Update work product
DELETE /admin/work/{work}/products/{id} - Remove product from work
```
### Stock Operations
```
POST /transaction/check-stock - Check stock availability
GET /transaction/stock-prediction - Get stock usage prediction
GET /admin/work/{work}/stock-prediction - Get work stock prediction
```
## StockService Methods
### `checkStockAvailability($workId, $dealerId, $workQuantity)`
Mengecek apakah dealer memiliki stock yang cukup untuk pekerjaan tertentu.
**Parameters:**
- `$workId`: ID pekerjaan
- `$dealerId`: ID dealer
- `$workQuantity`: Jumlah pekerjaan yang akan dilakukan
**Returns:**
```php
[
'available' => bool,
'message' => string,
'details' => [
[
'product_id' => int,
'product_name' => string,
'required_quantity' => float,
'available_stock' => float,
'is_available' => bool
]
]
]
```
### `reduceStockForTransaction($transaction)`
Mengurangi stock otomatis berdasarkan transaksi pekerjaan.
### `restoreStockForTransaction($transaction)`
Mengembalikan stock ketika transaksi dibatalkan/dihapus.
### `getStockUsagePrediction($workId, $quantity)`
Mendapatkan prediksi penggunaan stock untuk pekerjaan.
## User Interface
### 1. Work Products Management
- Akses melalui: **Admin Panel > Master > Pekerjaan > [Pilih Pekerjaan] > Tombol "Produk"**
- Fitur:
- Tambah/edit/hapus produk yang diperlukan
- Set quantity required per produk
- Tambah catatan untuk produk
- Preview prediksi penggunaan stock
### 2. Transaction Form dengan Stock Warning
- Real-time stock checking saat memilih pekerjaan
- Warning visual ketika stock tidak mencukupi
- Konfirmasi sebelum submit dengan stock negatif
### 3. Stock Prediction Modal
- Kalkulasi total produk yang dibutuhkan
- Informasi per produk dengan quantity dan satuan
## Usage Examples
### 1. Mengatur Produk untuk Pekerjaan "Service Rutin"
1. Masuk ke Admin Panel > Master > Pekerjaan
2. Klik tombol "Produk" pada pekerjaan "Service Rutin"
3. Klik "Tambah Produk"
4. Pilih produk "Oli Mesin", set quantity 4, notes "4 liter untuk ganti oli"
5. Tambah produk "Filter Oli", set quantity 1, notes "Filter standar"
6. Simpan
### 2. Membuat Transaksi dengan Stock Warning
1. Pada form transaksi, pilih pekerjaan "Service Rutin"
2. Set quantity 2 (untuk 2 kendaraan)
3. Sistem akan menampilkan warning jika stock tidak cukup:
- Oli Mesin: Butuh 8 liter, Tersedia 5 liter
- Filter Oli: Butuh 2 unit, Tersedia 3 unit
4. User dapat memilih untuk melanjutkan atau membatalkan
### 3. Melihat Prediksi Stock
1. Di halaman Work Products, klik "Prediksi Stock"
2. Set jumlah pekerjaan (misal: 5)
3. Sistem menampilkan:
- Oli Mesin: 4 liter/pekerjaan × 5 = 20 liter total
- Filter Oli: 1 unit/pekerjaan × 5 = 5 unit total
## Stock Flow Process
### Saat Transaksi Dibuat:
1. User memilih pekerjaan dan quantity
2. Sistem check stock availability
3. Jika stock tidak cukup, tampilkan warning
4. User konfirmasi untuk melanjutkan
5. Transaksi disimpan dengan status 'completed'
6. Stock otomatis dikurangi sesuai konfigurasi work products
### Saat Transaksi Dihapus:
1. Sistem ambil data transaksi
2. Kembalikan stock sesuai dengan produk yang digunakan
3. Catat dalam stock log
4. Hapus transaksi
## Error Handling
### Stock Tidak Mencukupi:
- Tampilkan warning dengan detail produk
- Izinkan user untuk melanjutkan dengan konfirmasi
- Stock boleh menjadi negatif (business rule)
### Product Tidak Dikonfigurasi:
- Jika pekerjaan belum dikonfigurasi produknya, tidak ada pengurangan stock
- Transaksi tetap bisa dibuat normal
### Database Transaction:
- Semua operasi stock menggunakan database transaction
- Rollback otomatis jika ada error
## Best Practices
### 1. Konfigurasi Work Products
- Set quantity required yang akurat
- Gunakan notes untuk informasi tambahan
- Review berkala konfigurasi produk
### 2. Stock Management
- Monitor stock levels secara berkala
- Set minimum stock alerts
- Koordinasi dengan procurement team
### 3. Training User
- Berikan training tentang stock warnings
- Edukasi tentang impact stock negatif
- Prosedur escalation jika stock habis
## Troubleshooting
### Stock Tidak Berkurang Otomatis:
1. Cek konfigurasi work products
2. Pastikan produk memiliki stock record di dealer
3. Check error logs
### Error Saat Submit Transaksi:
1. Refresh halaman dan coba lagi
2. Check koneksi internet
3. Contact admin jika masih error
### Stock Calculation Salah:
1. Review konfigurasi quantity di work products
2. Check apakah ada duplikasi produk
3. Verify stock log untuk audit trail
## Monitoring & Reporting
### Stock Logs
Semua perubahan stock tercatat dalam `stock_logs` table dengan informasi:
- Source transaction
- Previous quantity
- New quantity
- Change amount
- Timestamp
- User who made the change
### Reports Available
- Stock usage by work type
- Stock movement history
- Negative stock alerts
- Product consumption analysis
## Future Enhancements
1. **Automated Stock Alerts**: Email notifications ketika stock di bawah minimum
2. **Batch Operations**: Update multiple work products sekaligus
3. **Stock Forecasting**: Prediksi kebutuhan stock berdasarkan historical data
4. **Mobile Interface**: Mobile-friendly interface untuk stock checking
5. **Integration**: Integration dengan sistem procurement/inventory external

View File

@@ -1,67 +1,71 @@
.uppercaseFontCss input, .uppercaseFontCss input,
.uppercaseFontCss textarea, .uppercaseFontCss textarea,
.uppercaseFontCss .filter-option-inner-inner{ .uppercaseFontCss .filter-option-inner-inner {
text-transform: uppercase; text-transform: uppercase;
} }
.nonecaseFontCss input, .nonecaseFontCss input,
.nonecaseFontCss textarea, .nonecaseFontCss textarea,
.nonecaseFontCss .filter-option-inner-inner{ .nonecaseFontCss .filter-option-inner-inner {
text-transform: none !important; text-transform: none !important;
} }
.wrapperbtnToAdmin{ .wrapperbtnToAdmin {
height: 100%; height: 100%;
padding-top: 16px; padding-top: 16px;
} }
.blockTtd{ .blockTtd {
text-align: center; text-align: center;
} }
.sectionPrint{ .sectionPrint {
padding: 20px; padding: 20px;
} }
.tablePartBengkel, .tableJasaBengkel, .tablePembayaran, .faktur-service-info, .kerja-service-info{ .tablePartBengkel,
.tableJasaBengkel,
.tablePembayaran,
.faktur-service-info,
.kerja-service-info {
width: 100%; width: 100%;
} }
.tandaTanganPrint{ .tandaTanganPrint {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
padding-top: 10px; padding-top: 10px;
padding-bottom: : 2px; padding-bottom: 2px;
min-height: 50px; min-height: 50px;
} }
.headTablePrint{ .headTablePrint {
border-bottom: 1px dashed #ccc; border-bottom: 1px dashed #ccc;
} }
.infoKendaraanDanMekanik{ .infoKendaraanDanMekanik {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
.headerPrint{ .headerPrint {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
.faktur-service-info td{ .faktur-service-info td {
padding-right: 10px; padding-right: 10px;
} }
.kerja-service-info td{ .kerja-service-info td {
padding-right: 10px; padding-right: 10px;
} }
.inputUppercase{ .inputUppercase {
text-transform:uppercase text-transform: uppercase;
} }
.btnAddDadakan{ .btnAddDadakan {
/*position: absolute;*/ /*position: absolute;*/
/*right: -4px;*/ /*right: -4px;*/
margin-left: 24px; margin-left: 24px;
} }
.product-item-harga-subtotal{ .product-item-harga-subtotal {
position: absolute; position: absolute;
right: 0; right: 0;
} }
.product-item-harga-total{ .product-item-harga-total {
position: relative; position: relative;
font-weight: bold; font-weight: bold;
} }
.text-harga-total{ .text-harga-total {
position: absolute; position: absolute;
right: 0; right: 0;
} }
.product-item { .product-item {
@@ -69,12 +73,12 @@
position: relative; position: relative;
} }
.product-item:last-of-type { .product-item:last-of-type {
margin-bottom: 0px; margin-bottom: 0px;
} }
.label-repeater-hd{ .label-repeater-hd {
display: none !important; display: none !important;
} }
.label-repeater-hd:first-of-type{ .label-repeater-hd:first-of-type {
display: block; display: block;
} }
@@ -84,7 +88,7 @@
.label.label-lg { .label.label-lg {
height: 24px; height: 24px;
width: 24px; width: 24px;
font-size: .9rem; font-size: 0.9rem;
width: 30px; width: 30px;
height: 30px; height: 30px;
padding: 2px 15px; padding: 2px 15px;
@@ -93,7 +97,7 @@
margin-right: 5px; margin-right: 5px;
} }
.label.label-rounded { .label.label-rounded {
border-radius: border-radius: 4px; border-radius: 4px;
} }
.label.label-waiting { .label.label-waiting {
color: #fff; color: #fff;
@@ -127,29 +131,29 @@
color: #fff; color: #fff;
background-color: #3699ff; background-color: #3699ff;
} }
.legendHeader{ .legendHeader {
margin-top: 54px !important; margin-top: 54px !important;
padding-top: 17px; padding-top: 17px;
} }
.legendBlock { .legendBlock {
display: inline-block; display: inline-block;
margin-right: 15px; margin-right: 15px;
} }
#content-order-list{ #content-order-list {
margin-top: 40px; margin-top: 40px;
} }
#content-order-list .kt-portlet__body{ #content-order-list .kt-portlet__body {
background: #fff; background: #fff;
color: #646c9a; color: #646c9a;
border-radius: 0px; border-radius: 0px;
} }
.btn{ .btn {
cursor: pointer !important; cursor: pointer !important;
} }
.wrapText{ .wrapText {
white-space: pre-line; white-space: pre-line;
} }
.blockTrxNote{ .blockTrxNote {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
margin-left: 10px; margin-left: 10px;
@@ -158,7 +162,83 @@
} }
@media print { @media print {
body, html { body,
font-family: 'Verdana'; html {
font-family: "Verdana";
} }
} }
/* SweetAlert Icon Centering - Global */
.swal2-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 auto !important;
}
.swal2-icon .swal2-icon-content {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
}
.swal2-popup {
text-align: center !important;
}
.swal2-title {
text-align: center !important;
}
.swal2-content {
text-align: center !important;
}
.swal2-actions {
justify-content: center !important;
}
/* Additional SweetAlert styling for better appearance */
.swal2-popup {
border-radius: 0.5rem !important;
font-family: inherit !important;
}
.swal2-title {
font-size: 1.5rem !important;
font-weight: 600 !important;
color: #495057 !important;
}
.swal2-content {
font-size: 1rem !important;
color: #6c757d !important;
}
.swal2-actions .swal2-confirm {
background-color: #007bff !important;
border-color: #007bff !important;
border-radius: 0.375rem !important;
padding: 0.5rem 1.5rem !important;
font-weight: 500 !important;
}
.swal2-actions .swal2-cancel {
background-color: #6c757d !important;
border-color: #6c757d !important;
border-radius: 0.375rem !important;
padding: 0.5rem 1.5rem !important;
font-weight: 500 !important;
}
.swal2-actions .swal2-confirm:hover {
background-color: #0056b3 !important;
border-color: #0056b3 !important;
}
.swal2-actions .swal2-cancel:hover {
background-color: #545b62 !important;
border-color: #545b62 !important;
}

View File

@@ -0,0 +1,498 @@
// Global variables
let workId,
indexUrl,
storeUrl,
stockPredictionUrl,
csrfToken,
table,
baseUrl,
showUrlTemplate,
updateUrlTemplate,
destroyUrlTemplate;
$(document).ready(function () {
// Get URLs from hidden inputs
workId = $('input[name="work_id"]').val();
indexUrl = $('input[name="index_url"]').val();
storeUrl = $('input[name="store_url"]').val();
stockPredictionUrl = $('input[name="stock_prediction_url"]').val();
csrfToken = $('input[name="csrf_token"]').val();
baseUrl = $('input[name="base_url"]').val();
showUrlTemplate = $('input[name="show_url_template"]').val();
updateUrlTemplate = $('input[name="update_url_template"]').val();
destroyUrlTemplate = $('input[name="destroy_url_template"]').val();
console.log("Initialized with:", {
workId,
indexUrl,
storeUrl,
stockPredictionUrl,
csrfToken: csrfToken ? "Set" : "Not set",
baseUrl,
showUrlTemplate,
updateUrlTemplate,
destroyUrlTemplate,
});
// Set up CSRF token for all AJAX requests
$.ajaxSetup({
headers: {
"X-CSRF-TOKEN": csrfToken,
},
});
// Initialize Select2
$("#product_id").select2({
placeholder: "-- Pilih Produk --",
allowClear: true,
width: "100%",
dropdownParent: $("#workProductModal"),
language: {
noResults: function () {
return "Tidak ada hasil yang ditemukan";
},
searching: function () {
return "Mencari...";
},
},
});
table = $("#workProductsTable").DataTable({
processing: true,
serverSide: true,
ajax: {
url: indexUrl,
data: function (d) {
console.log("DataTables request data:", d);
return d;
},
error: function (xhr, error, thrown) {
console.error("DataTables error:", xhr, error, thrown);
},
},
columns: [
{ data: "product_code", name: "product_code" },
{ data: "product_name", name: "product_name" },
{ data: "product_category", name: "product_category" },
{ data: "unit", name: "unit" },
{
data: "quantity_required",
name: "quantity_required",
className: "text-right",
},
{ data: "notes", name: "notes" },
{
data: "action",
name: "action",
orderable: false,
searchable: false,
className: "text-center",
width: "auto",
render: function (data, type, row) {
return data;
},
},
],
pageLength: 25,
responsive: true,
autoWidth: false,
columnDefs: [
{
targets: -1, // Action column
className: "text-center",
width: "auto",
orderable: false,
},
],
});
// Add Work Product
$("#addWorkProduct").click(function () {
$("#workProductForm")[0].reset();
$("#work_product_id").val("");
$("#modalHeading").text("Tambah Produk");
$("#workProductModal").modal("show");
// Reset Select2
$("#product_id").val("").trigger("change");
});
// Modal close events
$("#workProductModal").on("hidden.bs.modal", function () {
$("#workProductForm")[0].reset();
$("#work_product_id").val("");
// Reset Select2
$("#product_id").val("").trigger("change");
});
$("#stockPredictionModal").on("hidden.bs.modal", function () {
$("#stockPredictionContent").html("");
$("#prediction_quantity").val(1);
});
// Manual modal close handlers for better compatibility
$(document).on("click", '[data-dismiss="modal"]', function () {
console.log("Modal dismiss clicked");
$(this).closest(".modal").modal("hide");
});
// Close button click handler
$(document).on("click", ".modal .close", function () {
console.log("Modal close button clicked");
$(this).closest(".modal").modal("hide");
});
// Modal backdrop click handler
$(document).on("click", ".modal", function (e) {
if (e.target === this) {
console.log("Modal backdrop clicked");
$(this).modal("hide");
}
});
// ESC key handler for modals
$(document).on("keydown", function (e) {
if (e.keyCode === 27) {
console.log("ESC key pressed");
$(".modal.show").modal("hide");
}
});
// Force close modal function
window.closeModal = function (modalId) {
console.log("Force closing modal:", modalId);
$("#" + modalId).modal("hide");
};
// Additional modal close handlers
$(document).on(
"click",
'.btn-secondary[data-dismiss="modal"]',
function () {
console.log("Secondary button clicked");
$(this).closest(".modal").modal("hide");
}
);
// Bootstrap 4/5 compatible modal close
$(document).on("click", '.btn[data-dismiss="modal"]', function () {
console.log("Button with data-dismiss clicked");
$(this).closest(".modal").modal("hide");
});
// Alternative close button selector
$(document).on("click", ".modal-footer .btn-secondary", function () {
console.log("Footer secondary button clicked");
$(this).closest(".modal").modal("hide");
});
// Bootstrap 5 fallback
$(document).on("click", '[data-bs-dismiss="modal"]', function () {
console.log("Bootstrap 5 dismiss clicked");
$(this).closest(".modal").modal("hide");
});
// Generic secondary button in modal
$(document).on("click", ".modal .btn-secondary", function () {
console.log("Generic secondary button clicked");
$(this).closest(".modal").modal("hide");
});
// Show Stock Prediction
$("#showStockPrediction").click(function () {
console.log("Show stock prediction clicked");
loadStockPrediction();
$("#stockPredictionModal").modal("show");
});
// Apply prediction button
$("#applyPrediction").click(function () {
console.log("Apply prediction clicked");
loadStockPrediction();
});
// Submit Form
$("#workProductForm").submit(function (e) {
e.preventDefault();
let formData = $(this).serialize();
let workProductId = $("#work_product_id").val();
let url, method;
if (workProductId) {
// Update
const currentUpdateUrlTemplate =
updateUrlTemplate ||
$('input[name="update_url_template"]').val();
url = currentUpdateUrlTemplate.replace(":id", workProductId);
method = "PUT";
} else {
// Create
url = storeUrl;
method = "POST";
}
$.ajax({
url: url,
method: method,
data: formData,
success: function (response) {
$("#workProductModal").modal("hide");
table.ajax.reload();
if (typeof Swal !== "undefined") {
Swal.fire({
icon: "success",
title: "Berhasil!",
text: response.message,
timer: 2000,
showConfirmButton: false,
});
} else {
alert("Berhasil: " + response.message);
}
},
error: function (xhr) {
console.error("Error saving work product:", xhr);
let errors = xhr.responseJSON.errors || {};
let message = xhr.responseJSON.message || "Terjadi kesalahan";
if (typeof Swal !== "undefined") {
Swal.fire({
icon: "error",
title: "Error!",
text: message,
});
} else {
alert("Error: " + message);
}
},
});
});
function loadStockPrediction() {
let quantity = $("#prediction_quantity").val();
// Validate quantity
if (!quantity || quantity < 1) {
$("#stockPredictionContent").html(
'<div class="alert alert-warning">Masukkan jumlah pekerjaan yang valid (minimal 1).</div>'
);
return;
}
// Show loading state
$("#stockPredictionContent").html(
'<div class="text-center"><i class="fa fa-spinner fa-spin" style="display: inline-block; width: 16px; height: 16px; border: 2px solid #f3f3f3; border-top: 2px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></i> Memuat data...</div>'
);
// Add timeout for loading state
let loadingTimeout = setTimeout(function () {
$("#stockPredictionContent").html(
'<div class="text-center"><i class="fa fa-spinner fa-spin" style="display: inline-block; width: 16px; height: 16px; border: 2px solid #f3f3f3; border-top: 2px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></i> Memuat data... (mungkin memakan waktu beberapa saat)</div>'
);
}, 2000);
$.ajax({
url: stockPredictionUrl,
method: "GET",
data: { quantity: quantity },
timeout: 10000, // 10 second timeout
success: function (response) {
clearTimeout(loadingTimeout);
let html = "";
if (response.data.length === 0) {
html =
'<div class="alert alert-info">Belum ada produk yang dikonfigurasi untuk pekerjaan ini.</div>';
} else {
html =
'<div class="table-responsive"><table class="table table-bordered">';
html +=
"<thead><tr><th>Produk</th><th>Qty per Pekerjaan</th><th>Total Dibutuhkan</th><th>Catatan</th></tr></thead><tbody>";
response.data.forEach(function (item) {
html += "<tr>";
html +=
"<td>" +
item.product_code +
" - " +
item.product_name +
"</td>";
html +=
'<td class="text-right">' +
item.quantity_per_work +
" " +
item.unit +
"</td>";
html +=
'<td class="text-right"><strong>' +
item.total_quantity_needed +
" " +
item.unit +
"</strong></td>";
html += "<td>" + (item.notes || "-") + "</td>";
html += "</tr>";
});
html += "</tbody></table></div>";
}
$("#stockPredictionContent").html(html);
},
error: function (xhr, status, error) {
clearTimeout(loadingTimeout);
console.error(
"Error loading stock prediction:",
xhr,
status,
error
);
let errorMessage = "Gagal memuat prediksi stock.";
if (xhr.responseJSON && xhr.responseJSON.message) {
errorMessage = xhr.responseJSON.message;
} else if (status === "timeout") {
errorMessage = "Request timeout. Silakan coba lagi.";
} else if (status === "error") {
errorMessage =
"Terjadi kesalahan jaringan. Silakan coba lagi.";
}
$("#stockPredictionContent").html(
'<div class="alert alert-danger">' + errorMessage + "</div>"
);
},
});
}
});
// Edit Work Product - Using delegated event for dynamic content
$(document).on("click", ".btn-edit-work-product", function () {
let id = $(this).data("id");
editWorkProduct(id);
});
function editWorkProduct(id) {
// Get URL template from hidden input if not available globally
const currentShowUrlTemplate =
showUrlTemplate || $('input[name="show_url_template"]').val();
let url = currentShowUrlTemplate.replace(":id", id);
console.log("Edit URL:", url);
console.log("Work ID:", workId, "Product ID:", id);
console.log("Hidden inputs:", {
work_id: $('input[name="work_id"]').val(),
show_url_template: $('input[name="show_url_template"]').val(),
});
$.ajax({
url: url,
method: "GET",
success: function (response) {
console.log("Edit response:", response);
let data = response.data;
$("#work_product_id").val(data.id);
$("#product_id").val(data.product_id).trigger("change");
$("#quantity_required").val(data.quantity_required);
$("#notes").val(data.notes);
$("#modalHeading").text("Edit Produk");
$("#workProductModal").modal("show");
},
error: function (xhr) {
console.error("Error fetching work product:", xhr);
console.error("Response:", xhr.responseText);
console.error("Status:", xhr.status);
console.error("URL attempted:", url);
if (typeof Swal !== "undefined") {
Swal.fire({
icon: "error",
title: "Error!",
text: "Gagal mengambil data produk",
});
} else {
alert("Error: Gagal mengambil data produk");
}
},
});
}
// Delete Work Product - Using delegated event for dynamic content
$(document).on("click", ".btn-delete-work-product", function () {
let id = $(this).data("id");
deleteWorkProduct(id);
});
function deleteWorkProduct(id) {
if (typeof Swal !== "undefined") {
Swal.fire({
title: "Hapus Produk?",
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Ya, Hapus!",
cancelButtonText: "Batal",
}).then((result) => {
if (result.isConfirmed) {
performDelete(id);
}
});
} else {
if (
confirm("Hapus Produk? Data yang dihapus tidak dapat dikembalikan!")
) {
performDelete(id);
}
}
}
function performDelete(id) {
// Get URL template and csrfToken from hidden input if not available globally
const currentDestroyUrlTemplate =
destroyUrlTemplate || $('input[name="destroy_url_template"]').val();
const currentCsrfToken = csrfToken || $('input[name="csrf_token"]').val();
let url = currentDestroyUrlTemplate.replace(":id", id);
console.log("Delete URL:", url);
console.log("Work ID:", workId, "Product ID:", id);
$.ajax({
url: url,
method: "DELETE",
data: {
_token: currentCsrfToken,
},
success: function (response) {
console.log("Delete response:", response);
$("#workProductsTable").DataTable().ajax.reload();
if (typeof Swal !== "undefined") {
Swal.fire({
icon: "success",
title: "Berhasil!",
text: response.message,
timer: 2000,
showConfirmButton: false,
});
} else {
alert("Berhasil: " + response.message);
}
},
error: function (xhr) {
console.error("Error deleting work product:", xhr);
console.error("Response:", xhr.responseText);
console.error("Status:", xhr.status);
console.error("URL attempted:", url);
if (typeof Swal !== "undefined") {
Swal.fire({
icon: "error",
title: "Error!",
text: "Gagal menghapus produk",
});
} else {
alert("Error: Gagal menghapus produk");
}
},
});
}

View File

@@ -1,104 +1,221 @@
// Global variables
let ajaxUrl, storeUrl, table;
$.ajaxSetup({ $.ajaxSetup({
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
} },
}); });
var table = $('#kt_table').DataTable({ $(document).ready(function () {
processing: true, // Get URLs from hidden inputs
serverSide: true, ajaxUrl = $('input[name="ajax_url"]').val();
ajax: $("input[name='ajax_url']"), storeUrl = $('input[name="store_url"]').val();
columns: [
{data: 'category_name', name: 'c.name'}, // Initialize DataTable
{data: 'name', name: 'w.name'}, table = $("#kt_table").DataTable({
{data: 'shortname', name: 'w.shortname'}, processing: true,
{data: 'desc', name: 'w.desc'}, serverSide: true,
{data: 'action', name: 'action', orderable: false, searchable: false}, ajax: {
] url: ajaxUrl,
},
columns: [
{ data: "category_name", name: "c.name" },
{ data: "name", name: "w.name" },
{ data: "shortname", name: "w.shortname" },
{ data: "desc", name: "w.desc" },
{
data: "action",
name: "action",
orderable: false,
searchable: false,
className: "text-center",
width: "auto",
render: function (data, type, row) {
return data;
},
},
],
responsive: true,
autoWidth: false,
columnDefs: [
{
targets: -1, // Action column
className: "text-center",
width: "auto",
orderable: false,
},
],
});
// Initialize Select2
$("#category_id").select2({
placeholder: "-- Pilih Kategori --",
allowClear: true,
width: "100%",
dropdownParent: $("#workModal"),
language: {
noResults: function () {
return "Tidak ada hasil yang ditemukan";
},
searching: function () {
return "Mencari...";
},
},
});
// Modal close handlers
$(document).on("click", '[data-dismiss="modal"]', function () {
$(this).closest(".modal").modal("hide");
});
$(document).on("click", ".modal .close", function () {
$(this).closest(".modal").modal("hide");
});
$(document).on("click", ".modal", function (e) {
if (e.target === this) {
$(this).modal("hide");
}
});
$(document).on("keydown", function (e) {
if (e.keyCode === 27) {
$(".modal.show").modal("hide");
}
});
// Bootstrap 5 fallback
$(document).on("click", '[data-bs-dismiss="modal"]', function () {
$(this).closest(".modal").modal("hide");
});
// Alternative close button selectors
$(document).on("click", ".btn-secondary", function () {
if ($(this).closest(".modal").length) {
$(this).closest(".modal").modal("hide");
}
});
// Force close function
window.closeModal = function (modalId) {
$("#" + modalId).modal("hide");
};
// Modal hidden event
$("#workModal").on("hidden.bs.modal", function () {
$("#workForm").trigger("reset");
$("#category_id").val("").trigger("change");
$('#workForm input[name="_method"]').remove();
});
// Add Work
$("#addWork").click(function () {
$("#workModal").modal("show");
let form_action = storeUrl;
$("#workForm").attr("action", form_action);
$("#workForm input[name='_method']").remove();
$("#workForm").attr("data-form", "store");
$("#workForm").trigger("reset");
$("#workForm textarea[name='desc']").val("");
// Reset Select2
$("#category_id").val("").trigger("change");
});
// Submit Form
$("#workForm").submit(function (e) {
e.preventDefault();
let dataForm = $("#workForm").attr("data-form");
if (dataForm == "store") {
$.ajax({
url: $("#workForm").attr("action"),
type: "POST",
data: $("#workForm").serialize(),
success: function (res) {
$("#workModal").modal("hide");
$("#workForm").trigger("reset");
$("#category_id").val("").trigger("change");
table.ajax.reload();
Swal.fire({
icon: "success",
title: "Berhasil!",
text: "Data pekerjaan berhasil disimpan.",
timer: 2000,
showConfirmButton: false,
});
},
});
} else if (dataForm == "update") {
$.ajax({
url: $("#workForm").attr("action"),
type: "POST",
data: $("#workForm").serialize(),
success: function (res) {
$("#workModal").modal("hide");
$("#workForm").trigger("reset");
$("#category_id").val("").trigger("change");
table.ajax.reload();
Swal.fire({
icon: "success",
title: "Berhasil!",
text: "Data pekerjaan berhasil diupdate.",
timer: 2000,
showConfirmButton: false,
});
},
});
}
});
}); });
// Global functions for edit and delete
$("#addWork").click(function() {
$("#workModal").modal("show")
let form_action = $("input[name='store_url']").val()
$("#workForm").attr('action', form_action)
$("#workForm input[name='_method']").remove()
$("#workForm").attr('data-form', 'store')
$("#workForm").trigger("reset")
$("#workForm textarea[name='desc']").val("")
})
function destroyWork(id) { function destroyWork(id) {
let action = $("#destroyWork"+id).attr("data-action") let action = $("#destroyWork" + id).attr("data-action");
Swal.fire({ Swal.fire({
title: 'Hapus Pekerjaan?', title: "Hapus Pekerjaan?",
text: "Anda tidak akan bisa mengembalikannya!", text: "Anda tidak akan bisa mengembalikannya!",
showCancelButton: true, showCancelButton: true,
confirmButtonColor: '#d33', confirmButtonColor: "#d33",
cancelButtonColor: '#dedede', cancelButtonColor: "#dedede",
confirmButtonText: 'Hapus' confirmButtonText: "Hapus",
}).then((result) => { }).then((result) => {
if (result.value) { if (result.value) {
$.ajax({ $.ajax({
url: action, url: action,
type: 'POST', type: "POST",
data: { data: {
_token: $('meta[name="csrf-token"]').attr('content'), _token: $('meta[name="csrf-token"]').attr("content"),
_method: 'DELETE' _method: "DELETE",
}, },
success: function(res) { success: function (res) {
Swal.fire( Swal.fire(
'Dealer Dihapus!' "Berhasil!",
) "Pekerjaan berhasil dihapus.",
table.ajax.reload() "success"
} );
}) if (table) {
table.ajax.reload();
}
},
});
} }
}) });
} }
function editWork(id) { function editWork(id) {
let form_action = $("#editWork"+id).attr("data-action") let form_action = $("#editWork" + id).attr("data-action");
let edit_url = $("#editWork"+id).attr("data-url") let edit_url = $("#editWork" + id).attr("data-url");
$("#workModal").modal("show") $("#workModal").modal("show");
$("#workForm").append('<input type="hidden" name="_method" value="PUT">') $("#workForm").append('<input type="hidden" name="_method" value="PUT">');
$("#workForm").attr('action', form_action) $("#workForm").attr("action", form_action);
$("#workForm").attr('data-form', 'update') $("#workForm").attr("data-form", "update");
$.get(edit_url, function(res) { $.get(edit_url, function (res) {
$("#workForm input[name='name']").val(res.data.name) $("#workForm input[name='name']").val(res.data.name);
$("#workForm input[name='shortname']").val(res.data.shortname) $("#workForm input[name='shortname']").val(res.data.shortname);
$("#workForm textarea[name='desc']").html(res.data.desc) $("#workForm textarea[name='desc']").html(res.data.desc);
$("#workForm option[value='"+ res.data.category_id +"']").prop('selected', true); $("#workForm select[name='category_id']")
}) .val(res.data.category_id)
.trigger("change");
});
} }
$(document).ready(function () {
$("#workForm").submit(function(e) {
e.preventDefault();
let dataForm = $("#workForm").attr('data-form')
if(dataForm == 'store') {
$.ajax({
url: $('#workForm').attr("action"),
type: 'POST',
data: $('#workForm').serialize(),
success: function(res) {
$("#workModal").modal("hide")
$('#workForm').trigger("reset")
table.ajax.reload()
}
})
}else if(dataForm == 'update') {
$.ajax({
url: $('#workForm').attr("action"),
type: 'POST',
data: $('#workForm').serialize(),
success: function(res) {
$("#workModal").modal("hide")
$('#workForm').trigger("reset")
table.ajax.reload()
}
})
}
})
});

View File

@@ -0,0 +1,531 @@
@extends('layouts.backapp')
@section('content')
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
<div class="kt-portlet__head kt-portlet__head--lg">
<div class="kt-portlet__head-label">
<span class="kt-portlet__head-icon">
<i class="kt-font-brand flaticon2-box"></i>
</span>
<h3 class="kt-portlet__head-title">
Kelola Produk untuk Pekerjaan: {{ $work->name }}
</h3>
</div>
<div class="kt-portlet__head-toolbar">
<div class="kt-portlet__head-wrapper">
<div class="kt-portlet__head-actions">
<a href="{{ route('work.index') }}" class="btn btn-secondary btn-sm mr-2">
<i class="fa fa-arrow-left"></i> Kembali
</a>
<button type="button" class="btn btn-info btn-sm mr-2" id="showStockPrediction">
<i class="fa fa-chart-line"></i> Prediksi Stock
</button>
@can('create', $menus['work.index'] ?? null)
<button type="button" class="btn btn-bold btn-label-brand btn-sm" id="addWorkProduct">
<i class="fa fa-plus"></i> Tambah Produk
</button>
@endcan
</div>
</div>
</div>
</div>
<div class="kt-portlet__body">
<!-- Work Info -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fa fa-info-circle mr-2"></i>
Informasi Pekerjaan
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="work-info-item mb-3">
<label class="font-weight-bold text-muted mb-1">Nama Pekerjaan:</label>
<div class="work-info-value">{{ $work->name }}</div>
</div>
<div class="work-info-item mb-3">
<label class="font-weight-bold text-muted mb-1">Short Name:</label>
<div class="work-info-value">{{ $work->shortname }}</div>
</div>
</div>
<div class="col-md-6">
<div class="work-info-item mb-3">
<label class="font-weight-bold text-muted mb-1">Kategori:</label>
<div class="work-info-value">
@if($work->category)
<span class="badge badge-info">{{ $work->category->name }}</span>
@else
<span class="text-muted">-</span>
@endif
</div>
</div>
<div class="work-info-item mb-3">
<label class="font-weight-bold text-muted mb-1">Deskripsi:</label>
<div class="work-info-value">
@if($work->desc)
{{ $work->desc }}
@else
<span class="text-muted">Tidak ada deskripsi</span>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
<div class="table-responsive">
<!--begin: Datatable -->
<table class="table table-striped table-bordered table-hover table-checkable" id="workProductsTable">
<thead>
<tr>
<th>Kode Produk</th>
<th>Nama Produk</th>
<th>Kategori</th>
<th>Satuan</th>
<th>Qty Diperlukan</th>
<th>Catatan</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!--end: Datatable -->
</div>
</div>
</div>
<!--begin::Modal-->
<div class="modal fade" id="workProductModal" tabindex="-1" role="dialog" aria-labelledby="workProductModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<form id="workProductForm" class="kt-form">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalHeading">
<i class="fa fa-plus mr-2"></i>
Tambah Produk
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<input type="hidden" id="work_id" name="work_id" value="{{ $work->id }}">
<input type="hidden" id="work_product_id" name="work_product_id">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="product_id" class="font-weight-bold">
<i class="fa fa-box mr-1"></i>
Pilih Produk <span class="text-danger">*</span>
</label>
<select name="product_id" id="product_id" class="form-control select2" required>
<option value="">-- Pilih Produk --</option>
@foreach ($products as $product)
<option value="{{ $product->id }}" data-code="{{ $product->code }}" data-unit="{{ $product->unit }}">
{{ $product->code }} - {{ $product->name }} ({{ $product->unit }})
</option>
@endforeach
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="quantity_required" class="font-weight-bold">
<i class="fa fa-calculator mr-1"></i>
Jumlah Diperlukan <span class="text-danger">*</span>
</label>
<input type="number" class="form-control" id="quantity_required" name="quantity_required"
placeholder="0.00" step="0.01" min="0.01" required>
</div>
</div>
</div>
<div class="form-group">
<label for="notes" class="font-weight-bold">
<i class="fa fa-sticky-note mr-1"></i>
Catatan
</label>
<textarea name="notes" id="notes" rows="3" class="form-control"
placeholder="Catatan atau keterangan tambahan (opsional)"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Batal
</button>
<button type="submit" class="btn btn-primary" id="saveBtn">
Simpan
</button>
</div>
</div>
</form>
</div>
</div>
<!--end::Modal-->
<!-- Stock Prediction Modal -->
<div class="modal fade" id="stockPredictionModal" tabindex="-1" role="dialog" aria-labelledby="stockPredictionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Prediksi Penggunaan Stock</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="prediction_quantity">Jumlah Pekerjaan:</label>
<div class="input-group">
<input type="number" class="form-control" id="prediction_quantity" value="1" min="1">
<div class="input-group-append">
<button type="button" class="btn btn-primary" id="applyPrediction">
Terapkan
</button>
</div>
</div>
</div>
<div id="stockPredictionContent">
<!-- Content will be loaded via AJAX -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
<!-- Hidden inputs for JavaScript -->
<input type="hidden" name="work_id" value="{{ $work->id }}">
<input type="hidden" name="index_url" value="{{ route('work.products.index', $work->id) }}">
<input type="hidden" name="store_url" value="{{ route('work.products.store', $work->id) }}">
<input type="hidden" name="stock_prediction_url" value="{{ route('work.products.stock-prediction', $work->id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="base_url" value="{{ route('work.products.index', $work->id) }}">
<input type="hidden" name="show_url_template" value="{{ route('work.products.show', ['work' => $work->id, 'workProduct' => ':id']) }}">
<input type="hidden" name="update_url_template" value="{{ route('work.products.update', ['work' => $work->id, 'workProduct' => ':id']) }}">
<input type="hidden" name="destroy_url_template" value="{{ route('work.products.destroy', ['work' => $work->id, 'workProduct' => ':id']) }}">
@endsection
@section('javascripts')
<script src="{{ url('js/pages/back/master/work-products.js') }}" type="text/javascript"></script>
@endsection
@section('styles')
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.modal .close {
cursor: pointer;
opacity: 0.7;
}
.modal .close:hover {
opacity: 1;
}
.input-group-append .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Action button flex layout */
.d-flex.flex-row.gap-1 {
display: flex !important;
flex-direction: row !important;
gap: 0.25rem !important;
align-items: center;
flex-wrap: nowrap;
}
.d-flex.flex-row.gap-1 .btn {
white-space: nowrap;
margin: 0;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0.2rem;
}
.d-flex.flex-row.gap-1 .btn i {
margin-right: 0.25rem;
}
/* Work Info Styling */
.work-info-item {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 0.75rem;
}
.work-info-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.work-info-item label {
font-size: 0.875rem;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.work-info-value {
font-size: 1rem;
color: #495057;
font-weight: 500;
word-wrap: break-word;
}
.card-header.bg-primary {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
border: none;
}
.card-header.bg-primary h5 {
font-weight: 600;
}
.badge.badge-info {
background-color: #17a2b8;
color: white;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border-radius: 0.25rem;
}
/* Ensure DataTables doesn't break flex layout */
.dataTables_wrapper .dataTables_scrollBody .d-flex {
display: flex !important;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.d-flex.flex-row.gap-1 {
flex-direction: column !important;
gap: 0.125rem !important;
}
.d-flex.flex-row.gap-1 .btn {
width: 100%;
margin-bottom: 0.125rem;
}
.work-info-item {
margin-bottom: 1rem !important;
}
.card-body .row .col-md-6:first-child {
margin-bottom: 1rem;
}
}
/* Modal and Form Styling */
.modal-header {
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.modal-header h5 {
font-weight: 600;
color: #495057;
}
.form-group label {
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.form-group label i {
color: #6c757d;
}
.form-control {
border-radius: 0.375rem;
border: 1px solid #ced4da;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn {
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.15s ease-in-out;
}
.btn i {
font-size: 0.875rem;
}
/* Required field indicator */
.text-danger {
color: #dc3545 !important;
}
/* Placeholder styling */
.form-control::placeholder {
color: #6c757d;
opacity: 0.7;
}
/* Select2 Styling */
.select2-container--default .select2-selection--single {
border: 1px solid #ced4da;
border-radius: 0.375rem;
height: 38px;
line-height: 36px;
background-color: #fff;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
padding-left: 12px;
padding-right: 30px;
color: #495057;
font-size: 14px;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #6c757d;
opacity: 0.7;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: calc(1.5em + 0.75rem);
top: 0;
}
.select2-container--default .select2-selection--single .select2-selection__clear {
color: #6c757d;
margin-right: 25px;
font-weight: bold;
}
.select2-container--default .select2-selection--single .select2-selection__clear:hover {
color: #dc3545;
}
.select2-container--default.select2-container--focus .select2-selection--single {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.select2-dropdown {
border: 1px solid #ced4da;
border-radius: 0.375rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
background-color: #fff;
}
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #ced4da;
border-radius: 0.25rem;
padding: 8px 12px;
font-size: 14px;
}
.select2-container--default .select2-search--dropdown .select2-search__field:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: none;
}
.select2-container--default .select2-results__option {
padding: 8px 12px;
font-size: 14px;
color: #495057;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #007bff;
color: #fff;
}
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #e9ecef;
color: #495057;
}
.select2-container--default .select2-results__option[aria-selected=true]:hover {
background-color: #007bff;
color: #fff;
}
.select2-container {
width: 100% !important;
}
/* Ensure Select2 works properly in modal */
.modal .select2-container {
z-index: 9999;
}
.modal .select2-dropdown {
z-index: 9999;
}
/* Fix Select2 height to match form controls */
.select2-container--default .select2-selection--single {
height: calc(1.5em + 0.75rem + 2px);
line-height: 1.5;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 1.5;
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: calc(1.5em + 0.75rem);
top: 0;
}
/* SweetAlert Icon Centering */
.swal2-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 auto !important;
}
.swal2-icon .swal2-icon-content {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
}
.swal2-popup {
text-align: center !important;
}
.swal2-title {
text-align: center !important;
}
.swal2-content {
text-align: center !important;
}
.swal2-actions {
justify-content: center !important;
}
</style>
@endsection

View File

@@ -6,6 +6,243 @@
#nama{ #nama{
text-transform: uppercase; text-transform: uppercase;
} }
/* Action button flex layout */
.d-flex.flex-row.gap-1 {
display: flex !important;
flex-direction: row !important;
gap: 0.25rem !important;
align-items: center;
flex-wrap: nowrap;
}
.d-flex.flex-row.gap-1 .btn {
white-space: nowrap;
margin: 0;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0.2rem;
}
.d-flex.flex-row.gap-1 .btn i {
margin-right: 0.25rem;
}
/* Ensure DataTables doesn't break flex layout */
.dataTables_wrapper .dataTables_scrollBody .d-flex {
display: flex !important;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.d-flex.flex-row.gap-1 {
flex-direction: column !important;
gap: 0.125rem !important;
}
.d-flex.flex-row.gap-1 .btn {
width: 100%;
margin-bottom: 0.125rem;
}
}
/* Modal and Form Styling */
.modal-header {
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.modal-header h5 {
font-weight: 600;
color: #495057;
}
.form-group label {
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.form-group label i {
color: #6c757d;
}
.form-control {
border-radius: 0.375rem;
border: 1px solid #ced4da;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn {
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.15s ease-in-out;
}
.btn i {
font-size: 0.875rem;
}
/* Required field indicator */
.text-danger {
color: #dc3545 !important;
}
/* Placeholder styling */
.form-control::placeholder {
color: #6c757d;
opacity: 0.7;
}
/* Select2 Styling */
.select2-container--default .select2-selection--single {
border: 1px solid #ced4da;
border-radius: 0.375rem;
height: 38px;
line-height: 36px;
background-color: #fff;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
padding-left: 12px;
padding-right: 30px;
color: #495057;
font-size: 14px;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #6c757d;
opacity: 0.7;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
width: 30px;
}
.select2-container--default .select2-selection--single .select2-selection__clear {
color: #6c757d;
margin-right: 25px;
font-weight: bold;
}
.select2-container--default .select2-selection--single .select2-selection__clear:hover {
color: #dc3545;
}
.select2-container--default.select2-container--focus .select2-selection--single {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.select2-dropdown {
border: 1px solid #ced4da;
border-radius: 0.375rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
background-color: #fff;
}
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #ced4da;
border-radius: 0.25rem;
padding: 8px 12px;
font-size: 14px;
}
.select2-container--default .select2-search--dropdown .select2-search__field:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: none;
}
.select2-container--default .select2-results__option {
padding: 8px 12px;
font-size: 14px;
color: #495057;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #007bff;
color: #fff;
}
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #e9ecef;
color: #495057;
}
.select2-container--default .select2-results__option[aria-selected=true]:hover {
background-color: #007bff;
color: #fff;
}
.select2-container {
width: 100% !important;
}
/* Ensure Select2 works properly in modal */
.modal .select2-container {
z-index: 9999;
}
.modal .select2-dropdown {
z-index: 9999;
}
/* Fix Select2 height to match form controls */
.select2-container--default .select2-selection--single {
height: calc(1.5em + 0.75rem + 2px);
line-height: 1.5;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 1.5;
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: calc(1.5em + 0.75rem);
top: 0;
}
/* SweetAlert Icon Centering */
.swal2-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 auto !important;
}
.swal2-icon .swal2-icon-content {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
}
.swal2-popup {
text-align: center !important;
}
.swal2-title {
text-align: center !important;
}
.swal2-content {
text-align: center !important;
}
.swal2-actions {
justify-content: center !important;
}
</style> </style>
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable"> <div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
@@ -22,7 +259,7 @@
<div class="kt-portlet__head-toolbar"> <div class="kt-portlet__head-toolbar">
<div class="kt-portlet__head-wrapper"> <div class="kt-portlet__head-wrapper">
<div class="kt-portlet__head-actions"> <div class="kt-portlet__head-actions">
<button type="button" class="btn btn-bold btn-label-brand btn-sm" id="addWork"> Tambah </button> <button type="button" class="btn btn-bold btn-label-brand" id="addWork"> Tambah </button>
</div> </div>
</div> </div>
</div> </div>
@@ -52,41 +289,73 @@
<!--begin::Modal--> <!--begin::Modal-->
<div class="modal fade" id="workModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="workModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog modal-lg" role="document">
<form id="workForm" class="kt-form"> <form id="workForm" class="kt-form">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="modalHeading"></h5> <h5 class="modal-title" id="modalHeading">
<i class="fa fa-plus mr-2"></i>
Tambah Pekerjaan
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="kt-portlet__body"> <div class="row">
<div class="form-group"> <div class="col-md-6">
<label>Nama Pekerjaan</label> <div class="form-group">
<input type="text" class="form-control inputUppercase" id="name" name="name" placeholder="Masukan Nama Pekerjaan" value="" required="" autocomplete="off" /> <label for="name" class="font-weight-bold">
<i class="fa fa-tag mr-1"></i>
Nama Pekerjaan <span class="text-danger">*</span>
</label>
<input type="text" class="form-control inputUppercase" id="name" name="name"
placeholder="Masukan Nama Pekerjaan" value="" required="" autocomplete="off" />
</div>
</div> </div>
<div class="form-group"> <div class="col-md-6">
<label>Short Name</label> <div class="form-group">
<input type="text" class="form-control inputUppercase" id="shortname" name="shortname" placeholder="Masukan Short Name" value="" required="" autocomplete="off" /> <label for="shortname" class="font-weight-bold">
<i class="fa fa-code mr-1"></i>
Short Name <span class="text-danger">*</span>
</label>
<input type="text" class="form-control inputUppercase" id="shortname" name="shortname"
placeholder="Masukan Short Name" value="" required="" autocomplete="off" />
</div>
</div> </div>
<div class="form-group"> </div>
<label for="category_id">Kategori</label> <div class="row">
<select name="category_id" id="category_id" class="form-control"> <div class="col-md-6">
@foreach ($categories as $category) <div class="form-group">
<option value="{{ $category->id }}">{{ $category->name }}</option> <label for="category_id" class="font-weight-bold">
@endforeach <i class="fa fa-folder mr-1"></i>
</select> Kategori <span class="text-danger">*</span>
</div> </label>
<div class="form-group"> <select name="category_id" id="category_id" class="form-control select2" required>
<label for="desc">Deskripsi Pekerjaan</label> <option value="">-- Pilih Kategori --</option>
<textarea name="desc" id="desc" rows="5" class="form-control"></textarea> @foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
</div>
</div> </div>
</div> </div>
<div class="form-group">
<label for="desc" class="font-weight-bold">
<i class="fa fa-align-left mr-1"></i>
Deskripsi Pekerjaan
</label>
<textarea name="desc" id="desc" rows="4" class="form-control"
placeholder="Masukan deskripsi pekerjaan (opsional)"></textarea>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">
<button type="submit" class="btn btn-primary" id="saveBtn" value="create">Simpan</button> Batal
</button>
<button type="submit" class="btn btn-primary" id="saveBtn" value="create">
Simpan
</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -111,8 +111,8 @@ License: You must have a valid license purchased only from themeforest(the above
<!-- end::Scrolltop --> <!-- end::Scrolltop -->
<!--begin::Global Theme Bundle(used by all pages) --> <!--begin::Global Theme Bundle(used by all pages) -->
<script src="{{ asset('js/bootstrap-datepicker.min.js') }}"></script>
<script src="{{ asset('js/app.bundle.min.js') }}"></script> <script src="{{ asset('js/app.bundle.min.js') }}"></script>
<script src="{{ asset('js/bootstrap-datepicker.min.js') }}"></script>
<script src="//maps.google.com/maps/api/js?key=AIzaSyBTGnKT7dt597vo9QgeQ7BFhvSRP4eiMSM"></script> <script src="//maps.google.com/maps/api/js?key=AIzaSyBTGnKT7dt597vo9QgeQ7BFhvSRP4eiMSM"></script>
<!--end::Global Theme Bundle --> <!--end::Global Theme Bundle -->

View File

@@ -268,6 +268,65 @@ use Illuminate\Support\Facades\Auth;
width: 100% !important; width: 100% !important;
max-width: 100%; max-width: 100%;
} }
/* Style for placeholder options */
select option[disabled] {
color: #6c757d;
font-style: italic;
background-color: #f8f9fa;
}
/* Prevent auto-selection of first option */
select:focus option:first-child {
background-color: #f8f9fa;
}
/* Required field styling */
.form-control[required]:not(:placeholder-shown):valid {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
.form-control[required]:not(:placeholder-shown):invalid {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
/* Required field indicator */
.form-control[required]::placeholder {
color: #6c757d;
}
/* Service Advisor required styling */
select[name="user_sa_id"][required] option:first-child {
color: #6c757d;
font-style: italic;
}
/* Required field labels */
.form-group label:after {
content: " *";
color: #dc3545;
font-weight: bold;
}
/* Required field focus styling */
.form-control[required]:focus {
border-color: #5d78ff;
box-shadow: 0 0 0 0.2rem rgba(93, 120, 255, 0.25);
}
/* Invalid field styling */
.form-control.is-invalid {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
/* Valid field styling */
.form-control.is-valid {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style> </style>
@endsection @endsection
@@ -364,9 +423,31 @@ use Illuminate\Support\Facades\Auth;
<input type="hidden" name="form" value="work"> <input type="hidden" name="form" value="work">
<input type="hidden" name="mechanic_id" value="{{ $mechanic->id }}"> <input type="hidden" name="mechanic_id" value="{{ $mechanic->id }}">
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}"> <input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
<!-- Stock Error Display -->
@if($errors->has('stock'))
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<strong><i class="fa fa-exclamation-triangle"></i> Peringatan Stock:</strong>
<br>{!! $errors->first('stock') !!}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
@if($errors->has('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong><i class="fa fa-times-circle"></i> Error:</strong>
{{ $errors->first('error') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<input type="text" name="spk_no" class="form-control @if(old('form') == 'work') @error('spk_no') is-invalid @enderror @endif" value="{{ old('spk_no') }}" placeholder="No. SPK"> <label>No. SPK</label>
<input type="text" name="spk_no" class="form-control @if(old('form') == 'work') @error('spk_no') is-invalid @enderror @endif" value="{{ old('spk_no') }}" placeholder="No. SPK" required>
@if(old('form') == 'work') @if(old('form') == 'work')
@error('spk_no') @error('spk_no')
@@ -377,7 +458,8 @@ use Illuminate\Support\Facades\Auth;
@endif @endif
</div> </div>
<div class="col-6"> <div class="col-6">
<input type="text" name="police_number" class="form-control @if(old('form') == 'work') @error('police_number') is-invalid @enderror" @endif value="{{ old('police_number') }}" placeholder="No. Polisi"> <label>No. Polisi</label>
<input type="text" name="police_number" class="form-control @if(old('form') == 'work') @error('police_number') is-invalid @enderror" @endif value="{{ old('police_number') }}" placeholder="No. Polisi" required>
@if(old('form') == 'work') @if(old('form') == 'work')
@error('police_number') @error('police_number')
<span class="invalid-feedback" role="alert"> <span class="invalid-feedback" role="alert">
@@ -389,6 +471,7 @@ use Illuminate\Support\Facades\Auth;
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<label>Warranty</label>
<select name="warranty" class="form-control @if(old('form') == 'work') @error('warranty') is-invalid @enderror @endif"> <select name="warranty" class="form-control @if(old('form') == 'work') @error('warranty') is-invalid @enderror @endif">
<option selected>Warranty</option> <option selected>Warranty</option>
<option value="1" @if(old('form') == 'work') @if(old('warranty') == 1) selected @endif @endif>Ya</option> <option value="1" @if(old('form') == 'work') @if(old('warranty') == 1) selected @endif @endif>Ya</option>
@@ -403,6 +486,7 @@ use Illuminate\Support\Facades\Auth;
@endif @endif
</div> </div>
<div class="col-6"> <div class="col-6">
<label>Tanggal Pekerjaan</label>
<input type="text" name="date" id="date-work" required class="form-control @if(old('form') == 'work') @error('date') is-invalid @enderror @endif" placeholder="Tanggal Pekerjaan"> <input type="text" name="date" id="date-work" required class="form-control @if(old('form') == 'work') @error('date') is-invalid @enderror @endif" placeholder="Tanggal Pekerjaan">
@if(old('form') == 'work') @if(old('form') == 'work')
@error('date') @error('date')
@@ -414,7 +498,8 @@ use Illuminate\Support\Facades\Auth;
</div> </div>
</div> </div>
<div class="form-group mt-4"> <div class="form-group mt-4">
<select name="user_sa_id" class="form-control @if(old('form') == 'work') @error('user_sa_id') is-invalid @enderror @endif"> <label>Service Advisor</label>
<select name="user_sa_id" class="form-control @if(old('form') == 'work') @error('user_sa_id') is-invalid @enderror @endif" required>
<option value="" selected>Service Advisor</option> <option value="" selected>Service Advisor</option>
@foreach ($user_sas as $user_sa) @foreach ($user_sas as $user_sa)
<option @if(old('form') == 'work') @if($user_sa->id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }}</option> <option @if(old('form') == 'work') @if($user_sa->id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }}</option>
@@ -436,10 +521,10 @@ use Illuminate\Support\Facades\Auth;
@for ($i = 0; $i < count(old('work_id')); $i++) @for ($i = 0; $i < count(old('work_id')); $i++)
<div class="form-group row" id="work_field{{ $i+1 }}"> <div class="form-group row" id="work_field{{ $i+1 }}">
<div class="col-6"> <div class="col-6">
<select name="work_id[]" id="work_work{{ $i+1 }}" class="form-control @error('work_id.'.$i)" is-invalid @enderror> <select name="work_id[]" id="work_work{{ $i+1 }}" class="form-control @error('work_id.'.$i) is-invalid @enderror">
<option selected disabled>Pekerjaan</option> <option value="" disabled>Pekerjaan</option>
@foreach ($work_works as $work) @foreach ($work_works as $work)
<option value="{{ $work->id }}" @if($work->id == old('work_id.'.$i)) selected @endif>{{ $work->name }}</option> <option value="{{ $work->id }}" @if($work->id == old('work_id.'.$i)) selected @endif>{{ $work->name }}</option>
@endforeach @endforeach
</select> </select>
@@ -467,7 +552,7 @@ use Illuminate\Support\Facades\Auth;
<div class="form-group row" id="work_field1"> <div class="form-group row" id="work_field1">
<div class="col-6"> <div class="col-6">
<select name="work_id[]" id="work_work1" class="form-control"> <select name="work_id[]" id="work_work1" class="form-control">
<option selected disabled>Pekerjaan</option> <option value="" selected disabled>Pekerjaan</option>
@foreach ($work_works as $work) @foreach ($work_works as $work)
<option value="{{ $work->id }}">{{ $work->name }}</option> <option value="{{ $work->id }}">{{ $work->name }}</option>
@endforeach @endforeach
@@ -483,7 +568,7 @@ use Illuminate\Support\Facades\Auth;
<div class="form-group row" id="work_field2"> <div class="form-group row" id="work_field2">
<div class="col-6"> <div class="col-6">
<select name="work_id[]" id="work_work2" class="form-control"> <select name="work_id[]" id="work_work2" class="form-control">
<option selected disabled>Pekerjaan</option> <option value="" selected disabled>Pekerjaan</option>
@foreach ($work_works as $work) @foreach ($work_works as $work)
<option value="{{ $work->id }}">{{ $work->name }}</option> <option value="{{ $work->id }}">{{ $work->name }}</option>
@endforeach @endforeach
@@ -499,7 +584,7 @@ use Illuminate\Support\Facades\Auth;
<div class="form-group row" id="work_field3"> <div class="form-group row" id="work_field3">
<div class="col-6"> <div class="col-6">
<select name="work_id[]" id="work_work3" class="form-control"> <select name="work_id[]" id="work_work3" class="form-control">
<option selected disabled>Pekerjaan</option> <option value="" selected disabled>Pekerjaan</option>
@foreach ($work_works as $work) @foreach ($work_works as $work)
<option value="{{ $work->id }}">{{ $work->name }}</option> <option value="{{ $work->id }}">{{ $work->name }}</option>
@endforeach @endforeach
@@ -515,7 +600,7 @@ use Illuminate\Support\Facades\Auth;
<div class="form-group row" id="work_field4"> <div class="form-group row" id="work_field4">
<div class="col-6"> <div class="col-6">
<select name="work_id[]" id="work_work4" class="form-control"> <select name="work_id[]" id="work_work4" class="form-control">
<option selected disabled>Pekerjaan</option> <option value="" selected disabled>Pekerjaan</option>
@foreach ($work_works as $work) @foreach ($work_works as $work)
<option value="{{ $work->id }}">{{ $work->name }}</option> <option value="{{ $work->id }}">{{ $work->name }}</option>
@endforeach @endforeach
@@ -531,7 +616,7 @@ use Illuminate\Support\Facades\Auth;
<div class="form-group row" id="work_field5"> <div class="form-group row" id="work_field5">
<div class="col-6"> <div class="col-6">
<select name="work_id[]" id="work_work5" class="form-control"> <select name="work_id[]" id="work_work5" class="form-control">
<option selected disabled>Pekerjaan</option> <option value="" selected disabled>Pekerjaan</option>
@foreach ($work_works as $work) @foreach ($work_works as $work)
<option value="{{ $work->id }}">{{ $work->name }}</option> <option value="{{ $work->id }}">{{ $work->name }}</option>
@endforeach @endforeach
@@ -548,7 +633,7 @@ use Illuminate\Support\Facades\Auth;
<div class="row"> <div class="row">
<div class="col-10"></div> <div class="col-10"></div>
<div class="col-2"> <div class="col-2">
<button class="btn mb-4 btn-sm btn-primary float-right btn-add-field-work" style="width: 100%;" onclick="addFormField('work'); return false;">+</button> <button class="btn mb-4 btn-sm btn-primary float-right btn-add-field-work" style="width: 100%;" onclick="addFormFieldWithStockCheck('work'); return false;">+</button>
</div> </div>
</div> </div>
</div> </div>
@@ -565,7 +650,8 @@ use Illuminate\Support\Facades\Auth;
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}"> <input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<input type="text" name="spk_no" class="form-control @if(old('form') == 'wash') @error('spk_no') is-invalid @enderror @endif" value="{{ old('spk_no') }}" placeholder="No. SPK"> <label>No. SPK</label>
<input type="text" name="spk_no" class="form-control @if(old('form') == 'wash') @error('spk_no') is-invalid @enderror @endif" value="{{ old('spk_no') }}" placeholder="No. SPK" required>
@if(old('form') == 'wash') @if(old('form') == 'wash')
@error('spk_no') @error('spk_no')
<span class="invalid-feedback" role="alert"> <span class="invalid-feedback" role="alert">
@@ -575,7 +661,8 @@ use Illuminate\Support\Facades\Auth;
@endif @endif
</div> </div>
<div class="col-6"> <div class="col-6">
<input type="text" name="police_number" class="form-control @if(old('form') == 'wash') @error('police_number') is-invalid @enderror @endif" value="{{ old('police_number') }}" placeholder="No. Polisi"> <label>No. Polisi</label>
<input type="text" name="police_number" class="form-control @if(old('form') == 'wash') @error('police_number') is-invalid @enderror @endif" value="{{ old('police_number') }}" placeholder="No. Polisi" required>
@if(old('form') == 'wash') @if(old('form') == 'wash')
@error('police_number') @error('police_number')
<span class="invalid-feedback" role="alert"> <span class="invalid-feedback" role="alert">
@@ -587,6 +674,7 @@ use Illuminate\Support\Facades\Auth;
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<label>Warranty</label>
<select name="warranty" class="form-control @if(old('form') == 'wash') @error('warranty') is-invalid @enderror @endif"> <select name="warranty" class="form-control @if(old('form') == 'wash') @error('warranty') is-invalid @enderror @endif">
<option selected>Warranty</option> <option selected>Warranty</option>
<option value="1" @if(old('form') == 'wash') @if(old('warranty') == 1) selected @endif @endif>Ya</option> <option value="1" @if(old('form') == 'wash') @if(old('warranty') == 1) selected @endif @endif>Ya</option>
@@ -599,7 +687,8 @@ use Illuminate\Support\Facades\Auth;
@enderror @enderror
</div> </div>
<div class="col-6"> <div class="col-6">
<input type="text" id="date-wash" name="date" class="form-control @if(old('form') == 'wash') @error('date') is-invalid @enderror @endif" placeholder="Tanggal Pekerjaan"> <label>Tanggal Pekerjaan</label>
<input type="text" id="date-wash" name="date" class="form-control @if(old('form') == 'wash') @error('date') is-invalid @enderror @endif" placeholder="Tanggal Pekerjaan" required>
@if(old('form') == 'wash') @if(old('form') == 'wash')
@error('date') @error('date')
<span class="invalid-feedback" role="alert"> <span class="invalid-feedback" role="alert">
@@ -610,8 +699,9 @@ use Illuminate\Support\Facades\Auth;
</div> </div>
</div> </div>
<div class="form-group mt-4"> <div class="form-group mt-4">
<select name="user_sa_id" class="form-control @if(old('form') == 'wash') @error('user_sa_id') is-invalid @enderror @endif"> <label>Service Advisor</label>
<option selected>Service Advisor</option> <select name="user_sa_id" class="form-control @if(old('form') == 'wash') @error('user_sa_id') is-invalid @enderror @endif" required>
<option value="" selected>Service Advisor</option>
@foreach ($user_sas as $user_sa) @foreach ($user_sas as $user_sa)
<option @if(old('form') == 'wash') @if($user_sa->id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }}</option> <option @if(old('form') == 'wash') @if($user_sa->id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }}</option>
@endforeach @endforeach
@@ -896,7 +986,114 @@ use Illuminate\Support\Facades\Auth;
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
} }
}); });
// Global variables for stock checking
let dealerId = {{ $mechanic->dealer_id }};
let stockWarnings = {};
// Function to check stock availability for selected work
function checkStockAvailability(workId, quantity, callback) {
if (!workId || !quantity || quantity < 1) {
if (callback) callback(null);
return;
}
$.ajax({
url: "{{ route('transaction.check-stock') }}",
method: 'POST',
data: {
work_id: workId,
dealer_id: dealerId,
quantity: quantity,
_token: $('meta[name="csrf-token"]').attr('content')
},
success: function(response) {
if (callback) callback(response.data);
},
error: function(xhr) {
console.error('Error checking stock:', xhr);
if (callback) callback(null);
}
});
}
// Function to display stock warning
function displayStockWarning(fieldId, stockData) {
// Remove existing warning
$(`#stock-warning-${fieldId}`).remove();
if (!stockData || stockData.available) {
return; // No warning needed
}
let warningHtml = `
<div id="stock-warning-${fieldId}" class="alert alert-warning mt-2 mb-0" style="font-size: 12px;">
<strong><i class="fa fa-exclamation-triangle"></i> Peringatan Stock:</strong> ${stockData.message}
<ul class="mb-0 mt-1" style="font-size: 11px;">
`;
stockData.details.forEach(function(detail) {
if (!detail.is_available) {
warningHtml += `
<li>${detail.product_name}: Butuh ${detail.required_quantity}, Tersedia ${detail.available_stock}</li>
`;
}
});
warningHtml += `
</ul>
</div>
`;
$(`#work_field${fieldId.replace('work_work', '')}`).append(warningHtml);
}
// Function to handle work selection change
function handleWorkSelectionChange(selectElement) {
let workId = $(selectElement).val();
let fieldId = $(selectElement).attr('id');
let quantityInput = $(selectElement).closest('.form-group').find('input[name="quantity[]"]');
let quantity = parseInt(quantityInput.val()) || 1;
if (workId) {
checkStockAvailability(workId, quantity, function(stockData) {
displayStockWarning(fieldId, stockData);
// Store warning data for form submission validation
if (stockData && !stockData.available) {
stockWarnings[fieldId] = stockData;
} else {
delete stockWarnings[fieldId];
}
});
} else {
// Remove warning when no work selected
$(`#stock-warning-${fieldId}`).remove();
delete stockWarnings[fieldId];
}
}
// Function to handle quantity change
function handleQuantityChange(quantityInput) {
let workSelect = $(quantityInput).closest('.form-group').find('select[name="work_id[]"]');
let workId = workSelect.val();
let quantity = parseInt($(quantityInput).val()) || 1;
if (workId && quantity > 0) {
let fieldId = workSelect.attr('id');
checkStockAvailability(workId, quantity, function(stockData) {
displayStockWarning(fieldId, stockData);
// Store warning data for form submission validation
if (stockData && !stockData.available) {
stockWarnings[fieldId] = stockData;
} else {
delete stockWarnings[fieldId];
}
});
}
}
function logout(event){ function logout(event){
event.preventDefault(); event.preventDefault();
@@ -973,23 +1170,296 @@ use Illuminate\Support\Facades\Auth;
function getWork(ajax_work_url, form, id) { function getWork(ajax_work_url, form, id) {
$.get(ajax_work_url, function(res) { $.get(ajax_work_url, function(res) {
var $select = $(`#${form}_work${id}`);
// Clear existing options except the first one (placeholder)
$select.find('option:not(:first)').remove();
// Add new options
$.each(res.data, function (i, item) { $.each(res.data, function (i, item) {
$(`#${form}_work${id}`).append($('<option>', { $select.append($('<option>', {
value: item.id, value: item.id,
text : item.name text : item.name
})); }));
}); });
// Ensure placeholder is still properly set
var $placeholder = $select.find('option:first');
if ($placeholder.length) {
$placeholder.prop('disabled', true).prop('selected', true).val('');
}
}) })
} }
// Add event listeners for existing fields
$(document).ready(function() {
// Initial fields (work1, work2, etc.)
$('select[name="work_id[]"]').on('change', function() {
handleWorkSelectionChange(this);
});
$('input[name="quantity[]"]').on('input', function() {
handleQuantityChange(this);
});
// Check stock for pre-filled fields
$('select[name="work_id[]"]').each(function() {
if ($(this).val()) {
handleWorkSelectionChange(this);
}
});
// Ensure placeholder options are properly disabled and not selectable
$('select[name="work_id[]"]').each(function() {
var $select = $(this);
var $placeholder = $select.find('option:first');
// Make sure placeholder is disabled and has empty value
if ($placeholder.length) {
$placeholder.prop('disabled', true).prop('selected', true).val('');
}
// Prevent auto-selection of first non-disabled option
$select.on('focus', function() {
if (!$(this).val()) {
$(this).find('option:first').prop('selected', true);
}
});
});
// Handle form errors - ensure work selections are maintained
@if($errors->any() && (old('form') == 'work' || old('form') == 'wash'))
// When there are form errors, ensure the correct work selections are maintained
// The old values are already handled by the Blade template in the HTML
// We just need to ensure proper placeholder handling
$('select[name="work_id[]"]').each(function() {
var $select = $(this);
// If no value is selected, ensure placeholder is selected
if (!$select.val() || $select.val() === '') {
var $placeholder = $select.find('option:first');
if ($placeholder.length) {
$placeholder.prop('selected', true);
}
} else {
// Trigger change event to update any dependent fields
$select.trigger('change');
}
});
@endif
});
// Override addFormField function to include event listeners
function addFormFieldWithStockCheck(form) {
addFormField(form); // Call original function
// Add event listeners to new field
setTimeout(function() {
var id = $(`.${form}_field_counter`).val();
var $newSelect = $(`#${form}_work${id}`);
// Ensure placeholder is properly set
var $placeholder = $newSelect.find('option:first');
if ($placeholder.length) {
$placeholder.prop('disabled', true).prop('selected', true).val('');
}
$newSelect.on('change', function() {
handleWorkSelectionChange(this);
});
$newSelect.closest('.form-group').find('input[name="quantity[]"]').on('input', function() {
handleQuantityChange(this);
});
// Prevent auto-selection of first non-disabled option
$newSelect.on('focus', function() {
if (!$(this).val()) {
$(this).find('option:first').prop('selected', true);
}
});
}, 100);
}
$("#workForm").submit(function(e) { $("#workForm").submit(function(e) {
$(".button-save").attr("disabled"); // Validate required fields
var spkNo = $('input[name="spk_no"]').val().trim();
var policeNumber = $('input[name="police_number"]').val().trim();
var userSaId = $('select[name="user_sa_id"]').val();
var date = $('input[name="date"]').val().trim();
var errorMessages = [];
if (!spkNo) {
errorMessages.push('No. SPK harus diisi');
$('input[name="spk_no"]').addClass('is-invalid');
} else {
$('input[name="spk_no"]').removeClass('is-invalid');
}
if (!policeNumber) {
errorMessages.push('No. Polisi harus diisi');
$('input[name="police_number"]').addClass('is-invalid');
} else {
$('input[name="police_number"]').removeClass('is-invalid');
}
if (!userSaId || userSaId === '') {
errorMessages.push('Service Advisor harus dipilih');
$('select[name="user_sa_id"]').addClass('is-invalid');
} else {
$('select[name="user_sa_id"]').removeClass('is-invalid');
}
if (!date) {
errorMessages.push('Tanggal Pekerjaan harus diisi');
$('input[name="date"]').addClass('is-invalid');
} else {
$('input[name="date"]').removeClass('is-invalid');
}
if (errorMessages.length > 0) {
e.preventDefault();
Swal.fire({
title: 'Validasi Gagal',
html: `
<div class="text-left">
<p class="mb-3">Mohon lengkapi field berikut:</p>
<ul class="text-left">
${errorMessages.map(msg => '<li>' + msg + '</li>').join('')}
</ul>
</div>
`,
icon: 'warning',
confirmButtonText: 'OK'
});
return false;
}
// Validate that at least one work is selected
var hasSelectedWork = false;
$('select[name="work_id[]"]').each(function() {
if ($(this).val() && $(this).val() !== '') {
hasSelectedWork = true;
return false; // break loop
}
});
if (!hasSelectedWork) {
e.preventDefault();
Swal.fire({
title: 'Peringatan',
text: 'Minimal pilih satu pekerjaan sebelum menyimpan!',
icon: 'warning',
confirmButtonText: 'OK'
});
return false;
}
// Check if there are stock warnings
if (Object.keys(stockWarnings).length > 0) {
e.preventDefault();
let warningMessages = [];
Object.values(stockWarnings).forEach(function(warning) {
warningMessages.push(warning.message);
warning.details.forEach(function(detail) {
if (!detail.is_available) {
warningMessages.push(`- ${detail.product_name}: Butuh ${detail.required_quantity}, Tersedia ${detail.available_stock}`);
}
});
});
Swal.fire({
title: 'Peringatan Stock Tidak Mencukupi',
html: `
<div class="text-left">
<p class="mb-3">Ada beberapa pekerjaan yang memerlukan produk dengan stock tidak mencukupi:</p>
<div class="alert alert-warning text-left">
${warningMessages.join('<br>')}
</div>
<p class="mb-0"><strong>Apakah Anda yakin ingin melanjutkan?</strong></p>
<small class="text-muted">Transaksi akan tetap dibuat, namun stock akan menjadi negatif.</small>
</div>
`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ffc107',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Lanjutkan',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
$(".button-save").attr("disabled", true);
$(".button-save").addClass("disabled");
$("#workForm")[0].submit(); // Submit the form
}
});
return false;
}
$(".button-save").attr("disabled", true);
$(".button-save").addClass("disabled"); $(".button-save").addClass("disabled");
return true; return true;
}) })
$("#washForm").submit(function(e) { $("#washForm").submit(function(e) {
$(".button-save").attr("disabled"); // Validate required fields
var spkNo = $('input[name="spk_no"]').val().trim();
var policeNumber = $('input[name="police_number"]').val().trim();
var userSaId = $('select[name="user_sa_id"]').val();
var date = $('input[name="date"]').val().trim();
var errorMessages = [];
if (!spkNo) {
errorMessages.push('No. SPK harus diisi');
$('input[name="spk_no"]').addClass('is-invalid');
} else {
$('input[name="spk_no"]').removeClass('is-invalid');
}
if (!policeNumber) {
errorMessages.push('No. Polisi harus diisi');
$('input[name="police_number"]').addClass('is-invalid');
} else {
$('input[name="police_number"]').removeClass('is-invalid');
}
if (!userSaId || userSaId === '') {
errorMessages.push('Service Advisor harus dipilih');
$('select[name="user_sa_id"]').addClass('is-invalid');
} else {
$('select[name="user_sa_id"]').removeClass('is-invalid');
}
if (!date) {
errorMessages.push('Tanggal Pekerjaan harus diisi');
$('input[name="date"]').addClass('is-invalid');
} else {
$('input[name="date"]').removeClass('is-invalid');
}
if (errorMessages.length > 0) {
e.preventDefault();
Swal.fire({
title: 'Validasi Gagal',
html: `
<div class="text-left">
<p class="mb-3">Mohon lengkapi field berikut:</p>
<ul class="text-left">
${errorMessages.map(msg => '<li>' + msg + '</li>').join('')}
</ul>
</div>
`,
icon: 'warning',
confirmButtonText: 'OK'
});
return false;
}
$(".button-save").attr("disabled", true);
$(".button-save").addClass("disabled"); $(".button-save").addClass("disabled");
return true; return true;
}) })
@@ -1280,28 +1750,31 @@ use Illuminate\Support\Facades\Auth;
} }
}) })
$("#date-work").datepicker({ // Initialize datepickers when document is ready
format: 'yyyy-mm-dd', $(document).ready(function() {
autoclose: true, $("#date-work").datepicker({
todayHighlight: true format: 'yyyy-mm-dd',
}) autoclose: true,
$("#date-wash").datepicker({ todayHighlight: true
format: 'yyyy-mm-dd', });
autoclose: true, $("#date-wash").datepicker({
todayHighlight: true format: 'yyyy-mm-dd',
}) autoclose: true,
$("#date-opname").datepicker({ todayHighlight: true
format: 'yyyy-mm-dd', });
autoclose: true, $("#date-opname").datepicker({
todayHighlight: true, format: 'yyyy-mm-dd',
startDate: '-30d', autoclose: true,
endDate: '+0d' todayHighlight: true,
}) startDate: '-30d',
$("#date-mutasi").datepicker({ endDate: '+0d'
format: 'yyyy-mm-dd', });
autoclose: true, $("#date-mutasi").datepicker({
todayHighlight: true format: 'yyyy-mm-dd',
}) autoclose: true,
todayHighlight: true
});
});
// Calculate difference for opname // Calculate difference for opname
$(document).on('input change keyup', '.physical-stock', function() { $(document).on('input change keyup', '.physical-stock', function() {
@@ -1393,6 +1866,15 @@ use Illuminate\Support\Facades\Auth;
$(this).removeClass('is-invalid'); $(this).removeClass('is-invalid');
}); });
// Remove invalid styling when user starts typing in required fields
$(document).on('input', 'input[name="spk_no"], input[name="police_number"], input[name="date"]', function() {
$(this).removeClass('is-invalid');
});
$(document).on('change', 'select[name="user_sa_id"]', function() {
$(this).removeClass('is-invalid');
});
// Handle server-side errors - scroll to first error and highlight // Handle server-side errors - scroll to first error and highlight
$(document).ready(function() { $(document).ready(function() {
// Set default date for opname if empty // Set default date for opname if empty
@@ -1431,6 +1913,18 @@ use Illuminate\Support\Facades\Auth;
initReceiveMutationsTable(); initReceiveMutationsTable();
}, 200); }, 200);
}, 100); }, 100);
@elseif($errors->any() && old('form') == 'work')
// Activate transaksi tab and form kerja sub-tab when there are work form errors
$('.nav-link[href="#transaksi"]').tab('show');
setTimeout(function() {
$('.nav-link[href="#form-kerja"]').tab('show');
}, 100);
@elseif($errors->any() && old('form') == 'wash')
// Activate transaksi tab and form cuci sub-tab when there are wash form errors
$('.nav-link[href="#transaksi"]').tab('show');
setTimeout(function() {
$('.nav-link[href="#form-cuci"]').tab('show');
}, 100);
@endif @endif
@endif @endif

View File

@@ -161,6 +161,10 @@ Route::group(['middleware' => 'auth'], function() {
Route::delete('/transaction/destory/{id}', [TransactionController::class, 'destroy'])->name('transaction.destroy'); Route::delete('/transaction/destory/{id}', [TransactionController::class, 'destroy'])->name('transaction.destroy');
Route::get('/transaction/edit/{id}', [TransactionController::class, 'edit'])->name('transaction.edit'); Route::get('/transaction/edit/{id}', [TransactionController::class, 'edit'])->name('transaction.edit');
Route::put('/transaction/update/{id}', [TransactionController::class, 'update'])->name('transaction.update'); Route::put('/transaction/update/{id}', [TransactionController::class, 'update'])->name('transaction.update');
// Stock Management Routes
Route::post('/transaction/check-stock', [TransactionController::class, 'checkStockAvailability'])->name('transaction.check-stock');
Route::get('/transaction/stock-prediction', [TransactionController::class, 'getStockPrediction'])->name('transaction.stock-prediction');
}); });
Route::group(['prefix' => 'admin', 'middleware' => 'adminRole'], function() { Route::group(['prefix' => 'admin', 'middleware' => 'adminRole'], function() {
@@ -176,6 +180,17 @@ Route::group(['middleware' => 'auth'], function() {
Route::resource('dealer', DealerController::class); Route::resource('dealer', DealerController::class);
Route::resource('category', CategoryController::class); Route::resource('category', CategoryController::class);
Route::resource('work', WorkController::class); Route::resource('work', WorkController::class);
// Work Products Management Routes
Route::prefix('work/{work}/products')->name('work.products.')->controller(App\Http\Controllers\WorkProductController::class)->group(function () {
Route::get('/', 'index')->name('index');
Route::post('/', 'store')->name('store');
Route::get('{workProduct}', 'show')->name('show');
Route::put('{workProduct}', 'update')->name('update');
Route::delete('{workProduct}', 'destroy')->name('destroy');
});
Route::get('work/{work}/stock-prediction', [App\Http\Controllers\WorkProductController::class, 'stockPrediction'])->name('work.products.stock-prediction');
Route::post('work/check-stock', [App\Http\Controllers\WorkProductController::class, 'checkStock'])->name('work.products.check-stock');
Route::get('/user', [UserController::class, 'index'])->name('user.index'); Route::get('/user', [UserController::class, 'index'])->name('user.index');
Route::post('/user', [UserController::class, 'store'])->name('user.store'); Route::post('/user', [UserController::class, 'store'])->name('user.store');
Route::delete('/user/{id}', [UserController::class, 'destroy'])->name('user.destroy'); Route::delete('/user/{id}', [UserController::class, 'destroy'])->name('user.destroy');