diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php
index b14ae08..c81ca8a 100755
--- a/app/Http/Controllers/TransactionController.php
+++ b/app/Http/Controllers/TransactionController.php
@@ -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('
', $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);
+ }
+ }
}
diff --git a/app/Http/Controllers/WorkController.php b/app/Http/Controllers/WorkController.php
index 4b84122..104cfb0 100755
--- a/app/Http/Controllers/WorkController.php
+++ b/app/Http/Controllers/WorkController.php
@@ -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 = '
';
- if(Auth::user()->can('delete', $menu)) {
- $btn .= '
';
+ // Products Management Button
+ if(Gate::allows('view', $menu)) {
+ $btn .= '
+ Produk
+ ';
}
- if(Auth::user()->can('update', $menu)) {
- $btn .= '
';
+ if(Gate::allows('update', $menu)) {
+ $btn .= '
';
}
+ if(Gate::allows('delete', $menu)) {
+ $btn .= '
';
+ }
+
+ $btn .= '
';
return $btn;
})
->rawColumns(['action'])
diff --git a/app/Http/Controllers/WorkProductController.php b/app/Http/Controllers/WorkProductController.php
new file mode 100644
index 0000000..903885d
--- /dev/null
+++ b/app/Http/Controllers/WorkProductController.php
@@ -0,0 +1,247 @@
+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 = '';
+
+ if(Gate::allows('update', $menu)) {
+ $btn .= '';
+ }
+
+ if(Gate::allows('delete', $menu)) {
+ $btn .= '';
+ }
+
+ $btn .= '
';
+ 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
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Product.php b/app/Models/Product.php
index de41a74..6c519f6 100755
--- a/app/Models/Product.php
+++ b/app/Models/Product.php
@@ -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);
+ }
}
diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php
index 678c106..f06f568 100755
--- a/app/Models/Transaction.php
+++ b/app/Models/Transaction.php
@@ -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');
}
}
diff --git a/app/Models/Work.php b/app/Models/Work.php
index fe59061..3eb6696 100755
--- a/app/Models/Work.php
+++ b/app/Models/Work.php
@@ -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);
+ }
}
diff --git a/app/Models/WorkProduct.php b/app/Models/WorkProduct.php
new file mode 100644
index 0000000..09e170e
--- /dev/null
+++ b/app/Models/WorkProduct.php
@@ -0,0 +1,32 @@
+ 'decimal:2'
+ ];
+
+ public function work()
+ {
+ return $this->belongsTo(Work::class);
+ }
+
+ public function product()
+ {
+ return $this->belongsTo(Product::class);
+ }
+}
\ No newline at end of file
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 7464ec5..4551eb4 100755
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -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();
+ });
}
/**
diff --git a/app/Services/StockService.php b/app/Services/StockService.php
new file mode 100644
index 0000000..cd68892
--- /dev/null
+++ b/app/Services/StockService.php
@@ -0,0 +1,197 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/database/migrations/2025_06_24_155937_create_work_products_table.php b/database/migrations/2025_06_24_155937_create_work_products_table.php
new file mode 100644
index 0000000..e484d97
--- /dev/null
+++ b/database/migrations/2025_06_24_155937_create_work_products_table.php
@@ -0,0 +1,38 @@
+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');
+ }
+}
diff --git a/database/seeders/WorkProductSeeder.php b/database/seeders/WorkProductSeeder.php
new file mode 100644
index 0000000..9a34886
--- /dev/null
+++ b/database/seeders/WorkProductSeeder.php
@@ -0,0 +1,77 @@
+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!');
+ }
+}
\ No newline at end of file
diff --git a/docs/WORK_PRODUCTS_STOCK_MANAGEMENT.md b/docs/WORK_PRODUCTS_STOCK_MANAGEMENT.md
new file mode 100644
index 0000000..de80691
--- /dev/null
+++ b/docs/WORK_PRODUCTS_STOCK_MANAGEMENT.md
@@ -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
diff --git a/public/css/custom-web.css b/public/css/custom-web.css
index f642383..30bd4f0 100755
--- a/public/css/custom-web.css
+++ b/public/css/custom-web.css
@@ -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";
}
-}
\ No newline at end of file
+}
+
+/* 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;
+}
diff --git a/public/js/pages/back/master/work-products.js b/public/js/pages/back/master/work-products.js
new file mode 100644
index 0000000..715e1fe
--- /dev/null
+++ b/public/js/pages/back/master/work-products.js
@@ -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(
+ 'Masukkan jumlah pekerjaan yang valid (minimal 1).
'
+ );
+ return;
+ }
+
+ // Show loading state
+ $("#stockPredictionContent").html(
+ ' Memuat data...
'
+ );
+
+ // Add timeout for loading state
+ let loadingTimeout = setTimeout(function () {
+ $("#stockPredictionContent").html(
+ ' Memuat data... (mungkin memakan waktu beberapa saat)
'
+ );
+ }, 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 =
+ 'Belum ada produk yang dikonfigurasi untuk pekerjaan ini.
';
+ } else {
+ html =
+ '';
+ html +=
+ "| Produk | Qty per Pekerjaan | Total Dibutuhkan | Catatan |
";
+
+ response.data.forEach(function (item) {
+ html += "";
+ html +=
+ "| " +
+ item.product_code +
+ " - " +
+ item.product_name +
+ " | ";
+ html +=
+ '' +
+ item.quantity_per_work +
+ " " +
+ item.unit +
+ " | ";
+ html +=
+ '' +
+ item.total_quantity_needed +
+ " " +
+ item.unit +
+ " | ";
+ html += "" + (item.notes || "-") + " | ";
+ html += "
";
+ });
+
+ html += "
";
+ }
+
+ $("#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(
+ '' + errorMessage + "
"
+ );
+ },
+ });
+ }
+});
+
+// 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");
+ }
+ },
+ });
+}
diff --git a/public/js/pages/back/master/work.js b/public/js/pages/back/master/work.js
index 7ad0b19..b48f269 100755
--- a/public/js/pages/back/master/work.js
+++ b/public/js/pages/back/master/work.js
@@ -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('')
- $("#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('');
+ $("#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()
- }
- })
- }
- })
-
-});
\ No newline at end of file
diff --git a/resources/views/back/master/work-products.blade.php b/resources/views/back/master/work-products.blade.php
new file mode 100644
index 0000000..196089f
--- /dev/null
+++ b/resources/views/back/master/work-products.blade.php
@@ -0,0 +1,531 @@
+@extends('layouts.backapp')
+
+@section('content')
+
+
+
+
+
+
+
+
+ Kelola Produk untuk Pekerjaan: {{ $work->name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $work->name }}
+
+
+
+
{{ $work->shortname }}
+
+
+
+
+
+
+ @if($work->category)
+ {{ $work->category->name }}
+ @else
+ -
+ @endif
+
+
+
+
+
+ @if($work->desc)
+ {{ $work->desc }}
+ @else
+ Tidak ada deskripsi
+ @endif
+
+
+
+
+
+
+
+
+
+
+
+
+ | Kode Produk |
+ Nama Produk |
+ Kategori |
+ Satuan |
+ Qty Diperlukan |
+ Catatan |
+ Aksi |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@endsection
+
+@section('javascripts')
+
+@endsection
+
+@section('styles')
+
+@endsection
\ No newline at end of file
diff --git a/resources/views/back/master/work.blade.php b/resources/views/back/master/work.blade.php
index e0f80bf..3c0c39c 100755
--- a/resources/views/back/master/work.blade.php
+++ b/resources/views/back/master/work.blade.php
@@ -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;
+ }
@@ -22,7 +259,7 @@
@@ -52,41 +289,73 @@