partial update transaction work with stock product
This commit is contained in:
@@ -9,17 +9,30 @@ use App\Models\Stock;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Models\Work;
|
||||
use App\Services\StockService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Exception;
|
||||
|
||||
class TransactionController extends Controller
|
||||
{
|
||||
protected $stockService;
|
||||
|
||||
public function __construct(StockService $stockService)
|
||||
{
|
||||
$this->stockService = $stockService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$work_works = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')->select('c.name as category_name', 'works.*')->where('c.name', 'LIKE', '%kerja%')->get();
|
||||
$work_works = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')
|
||||
->select('c.name as category_name', 'works.*')
|
||||
->where('c.name', 'LIKE', '%kerja%')
|
||||
->orderBy('works.name', 'asc')
|
||||
->get();
|
||||
$wash_work = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')->select('c.name as category_name', 'works.*')->where('c.name', 'LIKE', '%cuci%')->first();
|
||||
$user_sas = User::where('role_id', 4)->where('dealer_id', Auth::user()->dealer_id)->get();
|
||||
$count_transaction_users = Transaction::where("user_id", Auth::user()->id)->count();
|
||||
@@ -41,7 +54,9 @@ class TransactionController extends Controller
|
||||
|
||||
public function workcategory($category_id)
|
||||
{
|
||||
$works = Work::where('category_id', $category_id)->get();
|
||||
$works = Work::where('category_id', $category_id)
|
||||
->orderBy('name', 'asc')
|
||||
->get();
|
||||
$response = [
|
||||
"message" => "get work category successfully",
|
||||
"data" => $works,
|
||||
@@ -629,14 +644,28 @@ class TransactionController extends Controller
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
Transaction::find($id)->delete();
|
||||
|
||||
$response = [
|
||||
'message' => 'Data deleted successfully',
|
||||
'status' => 200
|
||||
];
|
||||
|
||||
return redirect()->back();
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$transaction = Transaction::find($id);
|
||||
|
||||
if (!$transaction) {
|
||||
return redirect()->back()->withErrors(['error' => 'Transaksi tidak ditemukan']);
|
||||
}
|
||||
|
||||
// Restore stock before deleting transaction
|
||||
$this->stockService->restoreStockForTransaction($transaction);
|
||||
|
||||
// Delete the transaction
|
||||
$transaction->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->back()->with('success', 'Transaksi berhasil dihapus dan stock telah dikembalikan');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
return redirect()->back()->withErrors(['error' => 'Gagal menghapus transaksi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -645,9 +674,19 @@ class TransactionController extends Controller
|
||||
$request->validate([
|
||||
'work_id.*' => ['required', 'integer'],
|
||||
'quantity.*' => ['required', 'integer'],
|
||||
'spk_no' => ['required', function($attribute, $value, $fail) use($request) {
|
||||
$date = explode('/', $request->date);
|
||||
$date = $date[2].'-'.$date[0].'-'.$date[1];
|
||||
'spk_no' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
|
||||
// Handle date format conversion safely for validation
|
||||
if (strpos($request->date, '/') !== false) {
|
||||
$dateParts = explode('/', $request->date);
|
||||
if (count($dateParts) === 3) {
|
||||
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
$fail('Format tanggal tidak valid');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$date = $request->date;
|
||||
}
|
||||
|
||||
if(!$request->work_id) {
|
||||
$fail('Pekerjaan harus diisi');
|
||||
@@ -665,9 +704,19 @@ class TransactionController extends Controller
|
||||
}
|
||||
}
|
||||
}],
|
||||
'police_number' => ['required', function($attribute, $value, $fail) use($request) {
|
||||
$date = explode('/', $request->date);
|
||||
$date = $date[2].'-'.$date[0].'-'.$date[1];
|
||||
'police_number' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
|
||||
// Handle date format conversion safely for validation
|
||||
if (strpos($request->date, '/') !== false) {
|
||||
$dateParts = explode('/', $request->date);
|
||||
if (count($dateParts) === 3) {
|
||||
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
$fail('Format tanggal tidak valid');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$date = $request->date;
|
||||
}
|
||||
|
||||
if(!$request->work_id) {
|
||||
$fail('Pekerjaan harus diisi');
|
||||
@@ -686,9 +735,19 @@ class TransactionController extends Controller
|
||||
}
|
||||
}],
|
||||
'warranty' => ['required'],
|
||||
'date' => ['required', function($attribute, $value, $fail) use($request) {
|
||||
$date = explode('/', $value);
|
||||
$date = $date[2].'-'.$date[0].'-'.$date[1];
|
||||
'date' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
|
||||
// Handle date format conversion safely for validation
|
||||
if (strpos($value, '/') !== false) {
|
||||
$dateParts = explode('/', $value);
|
||||
if (count($dateParts) === 3) {
|
||||
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
$fail('Format tanggal tidak valid. Gunakan format MM/DD/YYYY atau YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$date = $value;
|
||||
}
|
||||
|
||||
if(!$request->work_id) {
|
||||
$fail('Pekerjaan harus diisi');
|
||||
@@ -707,31 +766,117 @@ class TransactionController extends Controller
|
||||
}
|
||||
}],
|
||||
'category' => ['required'],
|
||||
'user_sa_id' => ['required', 'integer'],
|
||||
'user_sa_id' => ['required', 'integer', 'exists:users,id'],
|
||||
], [
|
||||
'spk_no.required' => 'No. SPK harus diisi',
|
||||
'spk_no.min' => 'No. SPK tidak boleh kosong',
|
||||
'police_number.required' => 'No. Polisi harus diisi',
|
||||
'police_number.min' => 'No. Polisi tidak boleh kosong',
|
||||
'date.required' => 'Tanggal Pekerjaan harus diisi',
|
||||
'date.min' => 'Tanggal Pekerjaan tidak boleh kosong',
|
||||
'user_sa_id.required' => 'Service Advisor harus dipilih',
|
||||
'user_sa_id.exists' => 'Service Advisor yang dipilih tidak valid',
|
||||
'work_id.*.required' => 'Pekerjaan harus dipilih',
|
||||
'quantity.*.required' => 'Quantity harus diisi',
|
||||
]);
|
||||
|
||||
$request['date'] = explode('/', $request->date);
|
||||
$request['date'] = $request['date'][2].'-'.$request['date'][0].'-'.$request['date'][1];
|
||||
|
||||
$data = [];
|
||||
for($i = 0; $i < count($request->work_id); $i++) {
|
||||
$data[] = [
|
||||
"user_id" => $request->mechanic_id,
|
||||
"dealer_id" => $request->dealer_id,
|
||||
"form" => $request->form,
|
||||
"work_id" => $request->work_id[$i],
|
||||
"qty" => $request->quantity[$i],
|
||||
"spk" => $request->spk_no,
|
||||
"police_number" => $request->police_number,
|
||||
"warranty" => $request->warranty,
|
||||
"user_sa_id" => $request->user_sa_id,
|
||||
"date" => $request->date,
|
||||
"created_at" => date('Y-m-d H:i:s')
|
||||
];
|
||||
// Handle date format conversion safely
|
||||
$dateValue = $request->date;
|
||||
if (strpos($dateValue, '/') !== false) {
|
||||
// If date is in MM/DD/YYYY format, convert to Y-m-d
|
||||
$dateParts = explode('/', $dateValue);
|
||||
if (count($dateParts) === 3) {
|
||||
$request['date'] = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
|
||||
} else {
|
||||
// Invalid date format, use as is
|
||||
$request['date'] = $dateValue;
|
||||
}
|
||||
} else {
|
||||
// Date is already in Y-m-d format or other format, use as is
|
||||
$request['date'] = $dateValue;
|
||||
}
|
||||
|
||||
Transaction::insert($data);
|
||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan');
|
||||
// Check stock availability for all works before creating transactions
|
||||
$stockErrors = [];
|
||||
for($i = 0; $i < count($request->work_id); $i++) {
|
||||
$stockCheck = $this->stockService->checkStockAvailability(
|
||||
$request->work_id[$i],
|
||||
$request->dealer_id,
|
||||
$request->quantity[$i]
|
||||
);
|
||||
|
||||
if (!$stockCheck['available']) {
|
||||
$work = Work::find($request->work_id[$i]);
|
||||
$stockErrors[] = "Pekerjaan '{$work->name}': {$stockCheck['message']}";
|
||||
|
||||
// Add detailed stock information
|
||||
if (!empty($stockCheck['details'])) {
|
||||
foreach ($stockCheck['details'] as $detail) {
|
||||
if (!$detail['is_available']) {
|
||||
$stockErrors[] = "- {$detail['product_name']}: Dibutuhkan {$detail['required_quantity']}, Tersedia {$detail['available_stock']}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are stock errors, return with error messages
|
||||
if (!empty($stockErrors)) {
|
||||
return redirect()->back()
|
||||
->withErrors(['stock' => implode('<br>', $stockErrors)])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$transactions = [];
|
||||
$data = [];
|
||||
|
||||
// Create transaction records
|
||||
for($i = 0; $i < count($request->work_id); $i++) {
|
||||
$transactionData = [
|
||||
"user_id" => $request->mechanic_id,
|
||||
"dealer_id" => $request->dealer_id,
|
||||
"form" => $request->form,
|
||||
"work_id" => $request->work_id[$i],
|
||||
"qty" => $request->quantity[$i],
|
||||
"spk" => $request->spk_no,
|
||||
"police_number" => $request->police_number,
|
||||
"warranty" => $request->warranty,
|
||||
"user_sa_id" => $request->user_sa_id,
|
||||
"date" => $request->date,
|
||||
"status" => 'completed', // Mark as completed to trigger stock reduction
|
||||
"created_at" => date('Y-m-d H:i:s'),
|
||||
"updated_at" => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$data[] = $transactionData;
|
||||
}
|
||||
|
||||
// Insert all transactions
|
||||
Transaction::insert($data);
|
||||
|
||||
// Get the created transactions for stock reduction
|
||||
$createdTransactions = Transaction::where('spk', $request->spk_no)
|
||||
->where('police_number', $request->police_number)
|
||||
->where('date', $request->date)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->get();
|
||||
|
||||
// Reduce stock for each transaction
|
||||
foreach ($createdTransactions as $transaction) {
|
||||
$this->stockService->reduceStockForTransaction($transaction);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
return redirect()->back()
|
||||
->withErrors(['error' => 'Gagal menyimpan transaksi: ' . $e->getMessage()])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
@@ -764,4 +909,62 @@ class TransactionController extends Controller
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability for work at dealer
|
||||
*/
|
||||
public function checkStockAvailability(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
try {
|
||||
$availability = $this->stockService->checkStockAvailability(
|
||||
$request->work_id,
|
||||
$request->dealer_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $availability
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Error checking stock: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock prediction for work
|
||||
*/
|
||||
public function getStockPrediction(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
try {
|
||||
$prediction = $this->stockService->getStockUsagePrediction(
|
||||
$request->work_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $prediction
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Error getting prediction: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,16 +26,28 @@ class WorkController extends Controller
|
||||
$data = DB::table('works as w')->leftJoin('categories as c', 'c.id', '=', 'w.category_id')->select('w.shortname as shortname', 'w.id as work_id', 'w.name as name', 'w.desc as desc', 'c.name as category_name', 'w.category_id as category_id');
|
||||
return DataTables::of($data)->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')"> Hapus </button>';
|
||||
// Products Management Button
|
||||
if(Gate::allows('view', $menu)) {
|
||||
$btn .= '<a href="'. route('work.products.index', ['work' => $row->work_id]) .'" class="btn btn-info btn-sm" title="Kelola Produk">
|
||||
Produk
|
||||
</a>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')"> Edit </button>';
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
|
||||
Edit
|
||||
</button>';
|
||||
}
|
||||
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')">
|
||||
Hapus
|
||||
</button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
|
||||
247
app/Http/Controllers/WorkProductController.php
Normal file
247
app/Http/Controllers/WorkProductController.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Work;
|
||||
use App\Models\Product;
|
||||
use App\Models\WorkProduct;
|
||||
use App\Models\Menu;
|
||||
use App\Services\StockService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\DataTables;
|
||||
|
||||
class WorkProductController extends Controller
|
||||
{
|
||||
protected $stockService;
|
||||
|
||||
public function __construct(StockService $stockService)
|
||||
{
|
||||
$this->stockService = $stockService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display work products for a specific work
|
||||
*/
|
||||
public function index(Request $request, $workId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$work = Work::with('category')->findOrFail($workId);
|
||||
|
||||
if ($request->ajax()) {
|
||||
Log::info('Work products index AJAX request for work ID: ' . $workId);
|
||||
|
||||
$workProducts = WorkProduct::with(['product', 'product.category'])
|
||||
->where('work_id', $workId)
|
||||
->get();
|
||||
|
||||
Log::info('Found ' . $workProducts->count() . ' work products');
|
||||
|
||||
return DataTables::of($workProducts)
|
||||
->addIndexColumn()
|
||||
->addColumn('product_name', function($row) {
|
||||
return $row->product->name;
|
||||
})
|
||||
->addColumn('product_code', function($row) {
|
||||
return $row->product->code;
|
||||
})
|
||||
->addColumn('product_category', function($row) {
|
||||
return $row->product->category ? $row->product->category->name : '-';
|
||||
})
|
||||
->addColumn('unit', function($row) {
|
||||
return $row->product->unit;
|
||||
})
|
||||
->addColumn('quantity_required', function($row) {
|
||||
return number_format($row->quantity_required, 2);
|
||||
})
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-edit-work-product" data-id="'.$row->id.'">
|
||||
Edit
|
||||
</button>';
|
||||
}
|
||||
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-delete-work-product" data-id="'.$row->id.'">
|
||||
Hapus
|
||||
</button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
$products = Product::where('active', true)->with('category')->get();
|
||||
|
||||
return view('back.master.work-products', compact('work', 'products'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store work product relationship
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('create', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'quantity_required' => 'required|numeric|min:0.01',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
// Check if combination already exists
|
||||
$exists = WorkProduct::where('work_id', $request->work_id)
|
||||
->where('product_id', $request->product_id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Produk sudah ditambahkan ke pekerjaan ini'
|
||||
], 422);
|
||||
}
|
||||
|
||||
WorkProduct::create($request->all());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Produk berhasil ditambahkan ke pekerjaan'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show work product for editing
|
||||
*/
|
||||
public function show($workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::with(['work', 'product', 'product.category'])
|
||||
->where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $workProduct
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update work product relationship
|
||||
*/
|
||||
public function update(Request $request, $workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('update', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'quantity_required' => 'required|numeric|min:0.01',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
$workProduct->update($request->only(['quantity_required', 'notes']));
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Data produk pekerjaan berhasil diupdate'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove work product relationship
|
||||
*/
|
||||
public function destroy($workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('delete', $menu), 403, 'Unauthorized User');
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
$workProduct->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Produk berhasil dihapus dari pekerjaan'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock prediction for work
|
||||
*/
|
||||
public function stockPrediction(Request $request, $workId)
|
||||
{
|
||||
$quantity = $request->get('quantity', 1);
|
||||
$prediction = $this->stockService->getStockUsagePrediction($workId, $quantity);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $prediction
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability for work at specific dealer
|
||||
*/
|
||||
public function checkStock(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$availability = $this->stockService->checkStockAvailability(
|
||||
$request->work_id,
|
||||
$request->dealer_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $availability
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,26 @@ class Product extends Model
|
||||
{
|
||||
return $this->stocks()->where('dealer_id', $dealerId)->first()?->quantity ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all works that require this product
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function works()
|
||||
{
|
||||
return $this->belongsToMany(Work::class, 'work_products')
|
||||
->withPivot('quantity_required', 'notes')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get work products pivot records
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workProducts()
|
||||
{
|
||||
return $this->hasMany(WorkProduct::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,40 @@ class Transaction extends Model
|
||||
/**
|
||||
* Get the work associated with the Transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function work()
|
||||
{
|
||||
return $this->hasOne(Work::class, 'id', 'work_id');
|
||||
return $this->belongsTo(Work::class, 'work_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dealer associated with the Transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SA user associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function userSa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_sa_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,36 @@ class Work extends Model
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'work_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all products required for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'work_products')
|
||||
->withPivot('quantity_required', 'notes')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get work products pivot records
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workProducts()
|
||||
{
|
||||
return $this->hasMany(WorkProduct::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category associated with the Work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Models/WorkProduct.php
Normal file
32
app/Models/WorkProduct.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkProduct extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'work_id',
|
||||
'product_id',
|
||||
'quantity_required',
|
||||
'notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity_required' => 'decimal:2'
|
||||
];
|
||||
|
||||
public function work()
|
||||
{
|
||||
return $this->belongsTo(Work::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(\App\Services\StockService::class, function ($app) {
|
||||
return new \App\Services\StockService();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
197
app/Services/StockService.php
Normal file
197
app/Services/StockService.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Stock;
|
||||
use App\Models\Work;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\StockLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Exception;
|
||||
|
||||
class StockService
|
||||
{
|
||||
/**
|
||||
* Check if dealer has sufficient stock for work
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $dealerId
|
||||
* @param int $workQuantity
|
||||
* @return array
|
||||
*/
|
||||
public function checkStockAvailability($workId, $dealerId, $workQuantity = 1)
|
||||
{
|
||||
$work = Work::with('products')->find($workId);
|
||||
|
||||
if (!$work) {
|
||||
return [
|
||||
'available' => false,
|
||||
'message' => 'Pekerjaan tidak ditemukan',
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
|
||||
$stockDetails = [];
|
||||
$allAvailable = true;
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$requiredQuantity = $product->pivot->quantity_required * $workQuantity;
|
||||
$availableStock = $product->getStockByDealer($dealerId);
|
||||
|
||||
$isAvailable = $availableStock >= $requiredQuantity;
|
||||
if (!$isAvailable) {
|
||||
$allAvailable = false;
|
||||
}
|
||||
|
||||
$stockDetails[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'required_quantity' => $requiredQuantity,
|
||||
'available_stock' => $availableStock,
|
||||
'is_available' => $isAvailable
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'available' => $allAvailable,
|
||||
'message' => $allAvailable ? 'Stock tersedia' : 'Stock tidak mencukupi',
|
||||
'details' => $stockDetails
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce stock when work transaction is completed
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function reduceStockForTransaction(Transaction $transaction)
|
||||
{
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
$work = $transaction->work;
|
||||
|
||||
if (!$work) {
|
||||
throw new Exception('Work not found for transaction');
|
||||
}
|
||||
|
||||
$work->load('products');
|
||||
|
||||
if ($work->products->isEmpty()) {
|
||||
// No products required for this work, return true
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$requiredQuantity = $product->pivot->quantity_required * $transaction->qty;
|
||||
|
||||
$stock = Stock::where('product_id', $product->id)
|
||||
->where('dealer_id', $transaction->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$stock) {
|
||||
throw new Exception("Stock not found for product {$product->name} at dealer");
|
||||
}
|
||||
|
||||
if ($stock->quantity < $requiredQuantity) {
|
||||
throw new Exception("Insufficient stock for product {$product->name}. Required: {$requiredQuantity}, Available: {$stock->quantity}");
|
||||
}
|
||||
|
||||
// Reduce stock
|
||||
$newQuantity = $stock->quantity - $requiredQuantity;
|
||||
$stock->updateStock(
|
||||
$newQuantity,
|
||||
$transaction,
|
||||
"Stock reduced for work: {$work->name} (Transaction #{$transaction->id})"
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore stock when work transaction is cancelled/reversed
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function restoreStockForTransaction(Transaction $transaction)
|
||||
{
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
$work = $transaction->work;
|
||||
|
||||
if (!$work) {
|
||||
throw new Exception('Work not found for transaction');
|
||||
}
|
||||
|
||||
$work->load('products');
|
||||
|
||||
if ($work->products->isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$restoreQuantity = $product->pivot->quantity_required * $transaction->qty;
|
||||
|
||||
$stock = Stock::where('product_id', $product->id)
|
||||
->where('dealer_id', $transaction->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$stock) {
|
||||
// Create new stock record if doesn't exist
|
||||
$stock = Stock::create([
|
||||
'product_id' => $product->id,
|
||||
'dealer_id' => $transaction->dealer_id,
|
||||
'quantity' => 0
|
||||
]);
|
||||
}
|
||||
|
||||
// Restore stock
|
||||
$newQuantity = $stock->quantity + $restoreQuantity;
|
||||
$stock->updateStock(
|
||||
$newQuantity,
|
||||
$transaction,
|
||||
"Stock restored from cancelled work: {$work->name} (Transaction #{$transaction->id})"
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock usage prediction for a work
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $quantity
|
||||
* @return array
|
||||
*/
|
||||
public function getStockUsagePrediction($workId, $quantity = 1)
|
||||
{
|
||||
$work = Work::with('products')->find($workId);
|
||||
|
||||
if (!$work) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$predictions = [];
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$totalRequired = $product->pivot->quantity_required * $quantity;
|
||||
|
||||
$predictions[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'product_code' => $product->code,
|
||||
'unit' => $product->unit,
|
||||
'quantity_per_work' => $product->pivot->quantity_required,
|
||||
'total_quantity_needed' => $totalRequired,
|
||||
'notes' => $product->pivot->notes
|
||||
];
|
||||
}
|
||||
|
||||
return $predictions;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
77
database/seeders/WorkProductSeeder.php
Normal file
77
database/seeders/WorkProductSeeder.php
Normal 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!');
|
||||
}
|
||||
}
|
||||
297
docs/WORK_PRODUCTS_STOCK_MANAGEMENT.md
Normal file
297
docs/WORK_PRODUCTS_STOCK_MANAGEMENT.md
Normal 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
|
||||
@@ -1,67 +1,71 @@
|
||||
.uppercaseFontCss input,
|
||||
.uppercaseFontCss textarea,
|
||||
.uppercaseFontCss .filter-option-inner-inner{
|
||||
.uppercaseFontCss .filter-option-inner-inner {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nonecaseFontCss input,
|
||||
.nonecaseFontCss textarea,
|
||||
.nonecaseFontCss .filter-option-inner-inner{
|
||||
.nonecaseFontCss .filter-option-inner-inner {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.wrapperbtnToAdmin{
|
||||
.wrapperbtnToAdmin {
|
||||
height: 100%;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.blockTtd{
|
||||
.blockTtd {
|
||||
text-align: center;
|
||||
}
|
||||
.sectionPrint{
|
||||
.sectionPrint {
|
||||
padding: 20px;
|
||||
}
|
||||
.tablePartBengkel, .tableJasaBengkel, .tablePembayaran, .faktur-service-info, .kerja-service-info{
|
||||
.tablePartBengkel,
|
||||
.tableJasaBengkel,
|
||||
.tablePembayaran,
|
||||
.faktur-service-info,
|
||||
.kerja-service-info {
|
||||
width: 100%;
|
||||
}
|
||||
.tandaTanganPrint{
|
||||
.tandaTanganPrint {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-top: 10px;
|
||||
padding-bottom: : 2px;
|
||||
padding-bottom: 2px;
|
||||
min-height: 50px;
|
||||
}
|
||||
.headTablePrint{
|
||||
.headTablePrint {
|
||||
border-bottom: 1px dashed #ccc;
|
||||
}
|
||||
.infoKendaraanDanMekanik{
|
||||
.infoKendaraanDanMekanik {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.headerPrint{
|
||||
.headerPrint {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.faktur-service-info td{
|
||||
.faktur-service-info td {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.kerja-service-info td{
|
||||
.kerja-service-info td {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.inputUppercase{
|
||||
text-transform:uppercase
|
||||
.inputUppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.btnAddDadakan{
|
||||
.btnAddDadakan {
|
||||
/*position: absolute;*/
|
||||
/*right: -4px;*/
|
||||
margin-left: 24px;
|
||||
}
|
||||
.product-item-harga-subtotal{
|
||||
position: absolute;
|
||||
.product-item-harga-subtotal {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.product-item-harga-total{
|
||||
.product-item-harga-total {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
}
|
||||
.text-harga-total{
|
||||
position: absolute;
|
||||
.text-harga-total {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.product-item {
|
||||
@@ -69,12 +73,12 @@
|
||||
position: relative;
|
||||
}
|
||||
.product-item:last-of-type {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.label-repeater-hd{
|
||||
.label-repeater-hd {
|
||||
display: none !important;
|
||||
}
|
||||
.label-repeater-hd:first-of-type{
|
||||
.label-repeater-hd:first-of-type {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@
|
||||
.label.label-lg {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: .9rem;
|
||||
font-size: 0.9rem;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 2px 15px;
|
||||
@@ -93,7 +97,7 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
.label.label-rounded {
|
||||
border-radius: border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.label.label-waiting {
|
||||
color: #fff;
|
||||
@@ -127,29 +131,29 @@
|
||||
color: #fff;
|
||||
background-color: #3699ff;
|
||||
}
|
||||
.legendHeader{
|
||||
margin-top: 54px !important;
|
||||
.legendHeader {
|
||||
margin-top: 54px !important;
|
||||
padding-top: 17px;
|
||||
}
|
||||
.legendBlock {
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
}
|
||||
#content-order-list{
|
||||
margin-top: 40px;
|
||||
#content-order-list {
|
||||
margin-top: 40px;
|
||||
}
|
||||
#content-order-list .kt-portlet__body{
|
||||
background: #fff;
|
||||
#content-order-list .kt-portlet__body {
|
||||
background: #fff;
|
||||
color: #646c9a;
|
||||
border-radius: 0px;
|
||||
}
|
||||
.btn{
|
||||
cursor: pointer !important;
|
||||
.btn {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.wrapText{
|
||||
white-space: pre-line;
|
||||
.wrapText {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.blockTrxNote{
|
||||
.blockTrxNote {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
margin-left: 10px;
|
||||
@@ -158,7 +162,83 @@
|
||||
}
|
||||
|
||||
@media print {
|
||||
body, html {
|
||||
font-family: 'Verdana';
|
||||
body,
|
||||
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;
|
||||
}
|
||||
|
||||
498
public/js/pages/back/master/work-products.js
Normal file
498
public/js/pages/back/master/work-products.js
Normal 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");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,104 +1,221 @@
|
||||
// Global variables
|
||||
let ajaxUrl, storeUrl, table;
|
||||
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
}
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||
},
|
||||
});
|
||||
|
||||
var table = $('#kt_table').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: $("input[name='ajax_url']"),
|
||||
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},
|
||||
]
|
||||
$(document).ready(function () {
|
||||
// Get URLs from hidden inputs
|
||||
ajaxUrl = $('input[name="ajax_url"]').val();
|
||||
storeUrl = $('input[name="store_url"]').val();
|
||||
|
||||
// Initialize DataTable
|
||||
table = $("#kt_table").DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#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("")
|
||||
})
|
||||
|
||||
// Global functions for edit and delete
|
||||
function destroyWork(id) {
|
||||
let action = $("#destroyWork"+id).attr("data-action")
|
||||
let action = $("#destroyWork" + id).attr("data-action");
|
||||
Swal.fire({
|
||||
title: 'Hapus Pekerjaan?',
|
||||
title: "Hapus Pekerjaan?",
|
||||
text: "Anda tidak akan bisa mengembalikannya!",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#dedede',
|
||||
confirmButtonText: 'Hapus'
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#dedede",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
$.ajax({
|
||||
url: action,
|
||||
type: 'POST',
|
||||
type: "POST",
|
||||
data: {
|
||||
_token: $('meta[name="csrf-token"]').attr('content'),
|
||||
_method: 'DELETE'
|
||||
_token: $('meta[name="csrf-token"]').attr("content"),
|
||||
_method: "DELETE",
|
||||
},
|
||||
success: function(res) {
|
||||
success: function (res) {
|
||||
Swal.fire(
|
||||
'Dealer Dihapus!'
|
||||
)
|
||||
table.ajax.reload()
|
||||
}
|
||||
})
|
||||
"Berhasil!",
|
||||
"Pekerjaan berhasil dihapus.",
|
||||
"success"
|
||||
);
|
||||
if (table) {
|
||||
table.ajax.reload();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function editWork(id) {
|
||||
let form_action = $("#editWork"+id).attr("data-action")
|
||||
let edit_url = $("#editWork"+id).attr("data-url")
|
||||
$("#workModal").modal("show")
|
||||
$("#workForm").append('<input type="hidden" name="_method" value="PUT">')
|
||||
$("#workForm").attr('action', form_action)
|
||||
$("#workForm").attr('data-form', 'update')
|
||||
$.get(edit_url, function(res) {
|
||||
$("#workForm input[name='name']").val(res.data.name)
|
||||
$("#workForm input[name='shortname']").val(res.data.shortname)
|
||||
$("#workForm textarea[name='desc']").html(res.data.desc)
|
||||
$("#workForm option[value='"+ res.data.category_id +"']").prop('selected', true);
|
||||
})
|
||||
let form_action = $("#editWork" + id).attr("data-action");
|
||||
let edit_url = $("#editWork" + id).attr("data-url");
|
||||
$("#workModal").modal("show");
|
||||
$("#workForm").append('<input type="hidden" name="_method" value="PUT">');
|
||||
$("#workForm").attr("action", form_action);
|
||||
$("#workForm").attr("data-form", "update");
|
||||
$.get(edit_url, function (res) {
|
||||
$("#workForm input[name='name']").val(res.data.name);
|
||||
$("#workForm input[name='shortname']").val(res.data.shortname);
|
||||
$("#workForm textarea[name='desc']").html(res.data.desc);
|
||||
$("#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()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
531
resources/views/back/master/work-products.blade.php
Normal file
531
resources/views/back/master/work-products.blade.php
Normal 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">×</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">×</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
|
||||
@@ -6,6 +6,243 @@
|
||||
#nama{
|
||||
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>
|
||||
|
||||
<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-wrapper">
|
||||
<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>
|
||||
@@ -52,41 +289,73 @@
|
||||
|
||||
<!--begin::Modal-->
|
||||
<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">
|
||||
<div class="modal-content">
|
||||
<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">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="kt-portlet__body">
|
||||
<div class="form-group">
|
||||
<label>Nama Pekerjaan</label>
|
||||
<input type="text" class="form-control inputUppercase" id="name" name="name" placeholder="Masukan Nama Pekerjaan" value="" required="" autocomplete="off" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label>Short Name</label>
|
||||
<input type="text" class="form-control inputUppercase" id="shortname" name="shortname" placeholder="Masukan Short Name" value="" required="" autocomplete="off" />
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label for="category_id">Kategori</label>
|
||||
<select name="category_id" id="category_id" class="form-control">
|
||||
@foreach ($categories as $category)
|
||||
<option value="{{ $category->id }}">{{ $category->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="desc">Deskripsi Pekerjaan</label>
|
||||
<textarea name="desc" id="desc" rows="5" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="category_id" class="font-weight-bold">
|
||||
<i class="fa fa-folder mr-1"></i>
|
||||
Kategori <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select name="category_id" id="category_id" class="form-control select2" required>
|
||||
<option value="">-- Pilih Kategori --</option>
|
||||
@foreach ($categories as $category)
|
||||
<option value="{{ $category->id }}">{{ $category->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</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 class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveBtn" value="create">Simpan</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
Batal
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveBtn" value="create">
|
||||
Simpan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -111,8 +111,8 @@ License: You must have a valid license purchased only from themeforest(the above
|
||||
<!-- end::Scrolltop -->
|
||||
|
||||
<!--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/bootstrap-datepicker.min.js') }}"></script>
|
||||
<script src="//maps.google.com/maps/api/js?key=AIzaSyBTGnKT7dt597vo9QgeQ7BFhvSRP4eiMSM"></script>
|
||||
<!--end::Global Theme Bundle -->
|
||||
|
||||
|
||||
@@ -268,6 +268,65 @@ use Illuminate\Support\Facades\Auth;
|
||||
width: 100% !important;
|
||||
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>
|
||||
@endsection
|
||||
|
||||
@@ -364,9 +423,31 @@ use Illuminate\Support\Facades\Auth;
|
||||
<input type="hidden" name="form" value="work">
|
||||
<input type="hidden" name="mechanic_id" value="{{ $mechanic->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">×</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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
<div class="form-group row">
|
||||
<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')
|
||||
@error('spk_no')
|
||||
@@ -377,7 +458,8 @@ use Illuminate\Support\Facades\Auth;
|
||||
@endif
|
||||
</div>
|
||||
<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')
|
||||
@error('police_number')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@@ -389,6 +471,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label>Warranty</label>
|
||||
<select name="warranty" class="form-control @if(old('form') == 'work') @error('warranty') is-invalid @enderror @endif">
|
||||
<option selected>Warranty</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
|
||||
</div>
|
||||
<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">
|
||||
@if(old('form') == 'work')
|
||||
@error('date')
|
||||
@@ -414,7 +498,8 @@ use Illuminate\Support\Facades\Auth;
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@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>
|
||||
@@ -436,10 +521,10 @@ use Illuminate\Support\Facades\Auth;
|
||||
@for ($i = 0; $i < count(old('work_id')); $i++)
|
||||
<div class="form-group row" id="work_field{{ $i+1 }}">
|
||||
<div class="col-6">
|
||||
<select name="work_id[]" id="work_work{{ $i+1 }}" class="form-control @error('work_id.'.$i)" is-invalid @enderror>
|
||||
<option selected disabled>Pekerjaan</option>
|
||||
<select name="work_id[]" id="work_work{{ $i+1 }}" class="form-control @error('work_id.'.$i) is-invalid @enderror">
|
||||
<option value="" disabled>Pekerjaan</option>
|
||||
@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
|
||||
</select>
|
||||
|
||||
@@ -467,7 +552,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
<div class="form-group row" id="work_field1">
|
||||
<div class="col-6">
|
||||
<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)
|
||||
<option value="{{ $work->id }}">{{ $work->name }}</option>
|
||||
@endforeach
|
||||
@@ -483,7 +568,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
<div class="form-group row" id="work_field2">
|
||||
<div class="col-6">
|
||||
<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)
|
||||
<option value="{{ $work->id }}">{{ $work->name }}</option>
|
||||
@endforeach
|
||||
@@ -499,7 +584,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
<div class="form-group row" id="work_field3">
|
||||
<div class="col-6">
|
||||
<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)
|
||||
<option value="{{ $work->id }}">{{ $work->name }}</option>
|
||||
@endforeach
|
||||
@@ -515,7 +600,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
<div class="form-group row" id="work_field4">
|
||||
<div class="col-6">
|
||||
<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)
|
||||
<option value="{{ $work->id }}">{{ $work->name }}</option>
|
||||
@endforeach
|
||||
@@ -531,7 +616,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
<div class="form-group row" id="work_field5">
|
||||
<div class="col-6">
|
||||
<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)
|
||||
<option value="{{ $work->id }}">{{ $work->name }}</option>
|
||||
@endforeach
|
||||
@@ -548,7 +633,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
<div class="row">
|
||||
<div class="col-10"></div>
|
||||
<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>
|
||||
@@ -565,7 +650,8 @@ use Illuminate\Support\Facades\Auth;
|
||||
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
|
||||
<div class="form-group row">
|
||||
<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')
|
||||
@error('spk_no')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@@ -575,7 +661,8 @@ use Illuminate\Support\Facades\Auth;
|
||||
@endif
|
||||
</div>
|
||||
<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')
|
||||
@error('police_number')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@@ -587,6 +674,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label>Warranty</label>
|
||||
<select name="warranty" class="form-control @if(old('form') == 'wash') @error('warranty') is-invalid @enderror @endif">
|
||||
<option selected>Warranty</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
|
||||
</div>
|
||||
<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')
|
||||
@error('date')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@@ -610,8 +699,9 @@ use Illuminate\Support\Facades\Auth;
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<option selected>Service Advisor</option>
|
||||
<label>Service Advisor</label>
|
||||
<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)
|
||||
<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
|
||||
@@ -896,7 +986,114 @@ use Illuminate\Support\Facades\Auth;
|
||||
headers: {
|
||||
'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){
|
||||
event.preventDefault();
|
||||
@@ -973,23 +1170,296 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
function getWork(ajax_work_url, form, id) {
|
||||
$.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) {
|
||||
$(`#${form}_work${id}`).append($('<option>', {
|
||||
$select.append($('<option>', {
|
||||
value: item.id,
|
||||
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) {
|
||||
$(".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");
|
||||
return true;
|
||||
})
|
||||
|
||||
$("#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");
|
||||
return true;
|
||||
})
|
||||
@@ -1280,28 +1750,31 @@ use Illuminate\Support\Facades\Auth;
|
||||
}
|
||||
})
|
||||
|
||||
$("#date-work").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
})
|
||||
$("#date-wash").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
})
|
||||
$("#date-opname").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true,
|
||||
startDate: '-30d',
|
||||
endDate: '+0d'
|
||||
})
|
||||
$("#date-mutasi").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
})
|
||||
// Initialize datepickers when document is ready
|
||||
$(document).ready(function() {
|
||||
$("#date-work").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
});
|
||||
$("#date-wash").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
});
|
||||
$("#date-opname").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true,
|
||||
startDate: '-30d',
|
||||
endDate: '+0d'
|
||||
});
|
||||
$("#date-mutasi").datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate difference for opname
|
||||
$(document).on('input change keyup', '.physical-stock', function() {
|
||||
@@ -1393,6 +1866,15 @@ use Illuminate\Support\Facades\Auth;
|
||||
$(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
|
||||
$(document).ready(function() {
|
||||
// Set default date for opname if empty
|
||||
@@ -1431,6 +1913,18 @@ use Illuminate\Support\Facades\Auth;
|
||||
initReceiveMutationsTable();
|
||||
}, 200);
|
||||
}, 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
|
||||
|
||||
|
||||
@@ -161,6 +161,10 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::delete('/transaction/destory/{id}', [TransactionController::class, 'destroy'])->name('transaction.destroy');
|
||||
Route::get('/transaction/edit/{id}', [TransactionController::class, 'edit'])->name('transaction.edit');
|
||||
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() {
|
||||
@@ -176,6 +180,17 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::resource('dealer', DealerController::class);
|
||||
Route::resource('category', CategoryController::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::post('/user', [UserController::class, 'store'])->name('user.store');
|
||||
Route::delete('/user/{id}', [UserController::class, 'destroy'])->name('user.destroy');
|
||||
|
||||
Reference in New Issue
Block a user