From 956df5cfe6496f53c79b2b1a0233aab618708969 Mon Sep 17 00:00:00 2001 From: arifal Date: Mon, 7 Jul 2025 19:11:04 +0700 Subject: [PATCH] create feature sa create list claim and price to work per dealer --- .../Controllers/TransactionController.php | 264 +++- app/Http/Controllers/WorkController.php | 23 + .../Controllers/WorkDealerPriceController.php | 363 +++++ app/Models/Dealer.php | 34 + app/Models/Transaction.php | 7 +- app/Models/Work.php | 48 + app/Models/WorkDealerPrice.php | 81 + app/Services/KpiService.php | 123 +- ..._claimed_columns_to_transactions_table.php | 33 + ...152055_create_work_dealer_prices_table.php | 40 + public/js/pages/back/master/work-prices.js | 655 ++++++++ .../views/back/master/work_prices.blade.php | 322 ++++ resources/views/back/users.blade.php | 34 +- .../layouts/partials/sidebarMenu.blade.php | 14 +- resources/views/transaction/index.blade.php | 1400 ++++++++++++----- routes/web.php | 25 + 16 files changed, 3062 insertions(+), 404 deletions(-) create mode 100644 app/Http/Controllers/WorkDealerPriceController.php create mode 100644 app/Models/WorkDealerPrice.php create mode 100644 database/migrations/2025_07_07_114022_add_claimed_columns_to_transactions_table.php create mode 100644 database/migrations/2025_07_07_152055_create_work_dealer_prices_table.php create mode 100644 public/js/pages/back/master/work-prices.js create mode 100644 resources/views/back/master/work_prices.blade.php diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 6635ff5..60b186c 100755 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -52,10 +52,10 @@ class TransactionController extends Controller // Get KPI data for current user using KPI service $kpiService = app(\App\Services\KpiService::class); - // Auto-calculate current month KPI achievement to ensure data is up-to-date - $kpiService->calculateKpiAchievement(Auth::user()); + // Auto-calculate current month KPI achievement including claimed transactions + $kpiService->calculateKpiAchievementWithClaims(Auth::user()); - $kpiSummary = $kpiService->getKpiSummary(Auth::user()); + $kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user()); // Get current month period name $currentMonthName = now()->translatedFormat('F Y'); @@ -896,7 +896,7 @@ class TransactionController extends Controller "warranty" => $request->warranty, "user_sa_id" => $request->user_sa_id, "date" => $request->date, - "status" => 'completed', // Mark as completed to trigger stock reduction + "status" => 0, // pending (0) - Mark as pending initially "created_at" => date('Y-m-d H:i:s'), "updated_at" => date('Y-m-d H:i:s') ]; @@ -919,6 +919,10 @@ class TransactionController extends Controller $this->stockService->reduceStockForTransaction($transaction); } + // Recalculate KPI achievement after creating transactions + $kpiService = app(\App\Services\KpiService::class); + $kpiService->calculateKpiAchievementWithClaims(Auth::user()); + DB::commit(); return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis'); @@ -1018,4 +1022,256 @@ class TransactionController extends Controller ], 500); } } + + /** + * Get claim transactions for DataTable - Only for mechanics + */ + public function getClaimTransactions(Request $request) + { + // Only allow mechanics to access this endpoint + if (Auth::user()->role_id != 3) { + return response()->json([ + 'draw' => intval($request->input('draw')), + 'recordsTotal' => 0, + 'recordsFiltered' => 0, + 'data' => [] + ]); + } + + $request->validate([ + 'dealer_id' => 'required|exists:dealers,id' + ]); + + try { + $query = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id') + ->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id') + ->leftJoin('works as w', 'w.id', '=', 'transactions.work_id') + ->select([ + 'transactions.id', + 'transactions.date', + 'transactions.spk', + 'transactions.police_number', + 'transactions.qty', + 'transactions.status', + 'transactions.claimed_at', + 'transactions.claimed_by', + 'w.name as work_name', + 'sa.name as sa_name', + 'users.name as mechanic_name' + ]) + ->where('transactions.dealer_id', $request->dealer_id) + ->where('users.role_id', 4) // Only transactions created by SA + ->whereIn('transactions.status', [0, 1]) // Only pending and completed transactions + ->orderBy('transactions.date', 'desc'); + + // Handle DataTables server-side processing + $total = $query->count(); + + // Search functionality + if ($request->has('search') && !empty($request->search['value'])) { + $searchValue = $request->search['value']; + $query->where(function($q) use ($searchValue) { + $q->where('transactions.spk', 'like', "%{$searchValue}%") + ->orWhere('transactions.police_number', 'like', "%{$searchValue}%") + ->orWhere('w.name', 'like', "%{$searchValue}%") + ->orWhere('sa.name', 'like', "%{$searchValue}%") + ->orWhere('users.name', 'like', "%{$searchValue}%"); + }); + } + + $filteredTotal = $query->count(); + + // Pagination + $start = $request->input('start', 0); + $length = $request->input('length', 15); + $query->skip($start)->take($length); + + $transactions = $query->get(); + + $data = []; + foreach ($transactions as $transaction) { + $data[] = [ + 'date' => date('d/m/Y', strtotime($transaction->date)), + 'spk' => $transaction->spk, + 'police_number' => $transaction->police_number, + 'work_name' => $transaction->work_name, + 'qty' => number_format($transaction->qty), + 'sa_name' => $transaction->sa_name, + 'status' => $this->getStatusBadge($transaction->status), + 'action' => $this->getActionButtons($transaction), + 'claimed_at' => $transaction->claimed_at, + 'claimed_by' => $transaction->claimed_by + ]; + } + + return response()->json([ + 'draw' => intval($request->input('draw')), + 'recordsTotal' => $total, + 'recordsFiltered' => $filteredTotal, + 'data' => $data + ]); + + } catch (Exception $e) { + return response()->json([ + 'error' => 'Error fetching claim transactions: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get status badge HTML + */ + private function getStatusBadge($status) + { + switch ($status) { + case 0: // pending + return 'Menunggu'; + case 1: // completed + return 'Selesai'; + case 2: // in_progress + return 'Sedang Dikerjakan'; + case 3: // claimed + return 'Diklaim'; + case 4: // cancelled + return 'Dibatalkan'; + default: + return 'Tidak Diketahui'; + } + } + + /** + * Claim a transaction - Only for mechanics + */ + public function claim($id) + { + // Only allow mechanics to claim transactions + if (Auth::user()->role_id != 3) { + return response()->json([ + 'status' => 403, + 'message' => 'Hanya mekanik yang dapat mengklaim pekerjaan' + ], 403); + } + + try { + $transaction = Transaction::find($id); + + if (!$transaction) { + return response()->json([ + 'status' => 404, + 'message' => 'Transaksi tidak ditemukan' + ], 404); + } + + // Check if transaction belongs to current user's dealer + if ($transaction->dealer_id !== Auth::user()->dealer_id) { + return response()->json([ + 'status' => 403, + 'message' => 'Anda tidak memiliki akses ke transaksi ini' + ], 403); + } + + // Check if transaction can be claimed (pending or completed) + if (!in_array($transaction->status, [0, 1])) { // pending (0) and completed (1) + return response()->json([ + 'status' => 400, + 'message' => 'Hanya transaksi yang menunggu atau sudah selesai yang dapat diklaim' + ], 400); + } + + // Check if transaction is already claimed + if (!empty($transaction->claimed_at) || !empty($transaction->claimed_by)) { + return response()->json([ + 'status' => 400, + 'message' => 'Transaksi ini sudah diklaim sebelumnya' + ], 400); + } + + // Check if transaction was created by SA (role_id = 4) + $creator = User::find($transaction->user_id); + if (!$creator || $creator->role_id != 4) { + return response()->json([ + 'status' => 400, + 'message' => 'Hanya transaksi yang dibuat oleh Service Advisor yang dapat diklaim' + ], 400); + } + + // Update transaction with claim information + $transaction->update([ + 'claimed_at' => now(), + 'claimed_by' => Auth::user()->id + ]); + + // Recalculate KPI achievement after claiming + $kpiService = app(\App\Services\KpiService::class); + $kpiService->calculateKpiAchievementWithClaims(Auth::user()); + + return response()->json([ + 'status' => 200, + 'message' => 'Pekerjaan berhasil diklaim' + ]); + + } catch (Exception $e) { + return response()->json([ + 'status' => 500, + 'message' => 'Gagal mengklaim pekerjaan: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get action buttons HTML for claim transactions - Only for mechanics + */ + private function getActionButtons($transaction) + { + $buttons = ''; + + // Only show buttons for mechanics + if (Auth::user()->role_id == 3) { + + // Claim button - show only if not claimed yet + if (empty($transaction->claimed_at) && empty($transaction->claimed_by)) { + $buttons .= ''; + } else { + $buttons .= 'Sudah Diklaim'; + } + } + + return $buttons; + } + + /** + * Get KPI data for AJAX refresh + */ + public function getKpiData() + { + try { + $kpiService = app(\App\Services\KpiService::class); + $kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user()); + + $currentMonthName = now()->translatedFormat('F Y'); + + $kpiData = [ + 'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0, + 'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0, + 'percentage' => $kpiSummary['current_percentage'], + 'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending', + 'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary', + 'period' => $currentMonthName, + 'has_target' => $kpiSummary['current_target'] ? true : false + ]; + + return response()->json([ + 'success' => true, + 'data' => $kpiData + ]); + + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error fetching KPI data: ' . $e->getMessage() + ], 500); + } + } } diff --git a/app/Http/Controllers/WorkController.php b/app/Http/Controllers/WorkController.php index 104cfb0..02bf87f 100755 --- a/app/Http/Controllers/WorkController.php +++ b/app/Http/Controllers/WorkController.php @@ -35,6 +35,13 @@ class WorkController extends Controller '; } + // Set Prices Button + if(Gate::allows('view', $menu)) { + $btn .= ' + Harga + '; + } + if(Gate::allows('update', $menu)) { $btn .= ''; + $btn .= ''; + $btn .= ''; + return $btn; + }) + ->rawColumns(['action']) + ->make(true); + } + + $dealers = Dealer::all(); + return view('back.master.work_prices', compact('work', 'dealers')); + } + + /** + * Store a newly created price + */ + public function store(Request $request) + { + try { + $request->validate([ + 'work_id' => 'required|exists:works,id', + 'dealer_id' => 'required|exists:dealers,id', + 'price' => 'required|numeric|min:0', + 'currency' => 'required|string|max:3', + 'is_active' => 'nullable|in:0,1', + ], [ + 'work_id.required' => 'ID pekerjaan harus diisi', + 'work_id.exists' => 'Pekerjaan tidak ditemukan', + 'dealer_id.required' => 'ID dealer harus diisi', + 'dealer_id.exists' => 'Dealer tidak ditemukan', + 'price.required' => 'Harga harus diisi', + 'price.numeric' => 'Harga harus berupa angka', + 'price.min' => 'Harga minimal 0', + 'currency.required' => 'Mata uang harus diisi', + 'currency.max' => 'Mata uang maksimal 3 karakter', + 'is_active.in' => 'Status aktif harus 0 atau 1', + ]); + + // Check if price already exists for this work-dealer combination (including soft deleted) + $existingPrice = WorkDealerPrice::withTrashed() + ->where('work_id', $request->work_id) + ->where('dealer_id', $request->dealer_id) + ->first(); + + // Also check for active records to prevent duplicates + $activePrice = WorkDealerPrice::where('work_id', $request->work_id) + ->where('dealer_id', $request->dealer_id) + ->where('id', '!=', $existingPrice ? $existingPrice->id : 0) + ->first(); + + if ($activePrice) { + return response()->json([ + 'status' => 422, + 'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.' + ], 422); + } + + // Use database transaction to prevent race conditions + DB::beginTransaction(); + try { + + if ($existingPrice) { + if ($existingPrice->trashed()) { + // Restore soft deleted record and update + $existingPrice->restore(); + } + + // Update existing price + $existingPrice->update([ + 'price' => $request->price, + 'currency' => $request->currency, + 'is_active' => $request->has('is_active') ? (bool)$request->is_active : true, + ]); + $price = $existingPrice; + + $message = 'Harga berhasil diperbarui'; + } else { + // Create new price + $price = WorkDealerPrice::create([ + 'work_id' => $request->work_id, + 'dealer_id' => $request->dealer_id, + 'price' => $request->price, + 'currency' => $request->currency, + 'is_active' => $request->has('is_active') ? (bool)$request->is_active : true, + ]); + + $message = 'Harga berhasil disimpan'; + } + + DB::commit(); + + return response()->json([ + 'status' => 200, + 'data' => $price, + 'message' => $message + ]); + + } catch (\Exception $e) { + DB::rollback(); + throw $e; + } + + } catch (\Illuminate\Validation\ValidationException $e) { + return response()->json([ + 'status' => 422, + 'message' => 'Validasi gagal', + 'errors' => $e->errors() + ], 422); + } catch (\Illuminate\Database\QueryException $e) { + // Handle unique constraint violation + if ($e->getCode() == 23000) { + return response()->json([ + 'status' => 422, + 'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.' + ], 422); + } + return response()->json([ + 'status' => 500, + 'message' => 'Terjadi kesalahan database: ' . $e->getMessage() + ], 500); + } catch (\Exception $e) { + return response()->json([ + 'status' => 500, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } + + /** + * Show the form for editing the specified price + */ + public function edit($id) + { + $price = WorkDealerPrice::with(['work', 'dealer'])->findOrFail($id); + + return response()->json([ + 'status' => 200, + 'data' => $price, + 'message' => 'Data harga berhasil diambil' + ]); + } + + /** + * Update the specified price + */ + public function update(Request $request, $id) + { + $request->validate([ + 'price' => 'required|numeric|min:0', + 'currency' => 'required|string|max:3', + 'is_active' => 'boolean', + ]); + + $price = WorkDealerPrice::findOrFail($id); + $price->update($request->all()); + + return response()->json([ + 'status' => 200, + 'message' => 'Harga berhasil diperbarui' + ]); + } + + /** + * Remove the specified price + */ + public function destroy($id) + { + try { + $price = WorkDealerPrice::findOrFail($id); + $price->delete(); // Soft delete + + return response()->json([ + 'status' => 200, + 'message' => 'Harga berhasil dihapus' + ]); + } catch (\Exception $e) { + return response()->json([ + 'status' => 500, + 'message' => 'Terjadi kesalahan saat menghapus harga: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get price for specific work and dealer + */ + public function getPrice(Request $request) + { + $request->validate([ + 'work_id' => 'required|exists:works,id', + 'dealer_id' => 'required|exists:dealers,id', + ]); + + $price = WorkDealerPrice::getPriceForWorkAndDealer( + $request->work_id, + $request->dealer_id + ); + + return response()->json([ + 'status' => 200, + 'data' => $price, + 'message' => $price ? 'Harga ditemukan' : 'Harga tidak ditemukan' + ]); + } + + /** + * Toggle status of a price + */ + public function toggleStatus(Request $request, Work $work) + { + try { + $request->validate([ + 'dealer_id' => 'required|exists:dealers,id', + 'is_active' => 'required|in:0,1,true,false', + ], [ + 'dealer_id.required' => 'ID dealer harus diisi', + 'dealer_id.exists' => 'Dealer tidak ditemukan', + 'is_active.required' => 'Status aktif harus diisi', + 'is_active.in' => 'Status aktif harus 0, 1, true, atau false', + ]); + + // Convert string values to boolean + $isActive = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN); + + // Find existing price (including soft deleted) + $existingPrice = WorkDealerPrice::withTrashed() + ->where('work_id', $work->id) + ->where('dealer_id', $request->dealer_id) + ->first(); + + if (!$existingPrice) { + // Create new record with default price 0 if no record exists + $existingPrice = WorkDealerPrice::create([ + 'work_id' => $work->id, + 'dealer_id' => $request->dealer_id, + 'price' => 0, + 'currency' => 'IDR', + 'is_active' => $isActive, + ]); + } else { + // Restore if soft deleted + if ($existingPrice->trashed()) { + $existingPrice->restore(); + } + + // Update status + $existingPrice->update([ + 'is_active' => $isActive + ]); + } + + return response()->json([ + 'status' => 200, + 'data' => $existingPrice, + 'message' => 'Status berhasil diubah menjadi ' . ($isActive ? 'Aktif' : 'Nonaktif') + ]); + + } catch (\Illuminate\Validation\ValidationException $e) { + return response()->json([ + 'status' => 422, + 'message' => 'Validasi gagal', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'status' => 500, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } + + /** + * Bulk create prices for a work + */ + public function bulkCreate(Request $request, Work $work) + { + $request->validate([ + 'prices' => 'required|array', + 'prices.*.dealer_id' => 'required|exists:dealers,id', + 'prices.*.price' => 'required|numeric|min:0', + 'prices.*.currency' => 'required|string|max:3', + ]); + + DB::beginTransaction(); + try { + foreach ($request->prices as $priceData) { + // Check if price already exists + $existingPrice = WorkDealerPrice::where('work_id', $work->id) + ->where('dealer_id', $priceData['dealer_id']) + ->first(); + + if ($existingPrice) { + // Update existing price + $existingPrice->update([ + 'price' => $priceData['price'], + 'currency' => $priceData['currency'], + 'is_active' => true, + ]); + } else { + // Create new price + WorkDealerPrice::create([ + 'work_id' => $work->id, + 'dealer_id' => $priceData['dealer_id'], + 'price' => $priceData['price'], + 'currency' => $priceData['currency'], + 'is_active' => true, + ]); + } + } + + DB::commit(); + + return response()->json([ + 'status' => 200, + 'message' => 'Harga berhasil disimpan' + ]); + + } catch (\Exception $e) { + DB::rollback(); + return response()->json([ + 'status' => 500, + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Models/Dealer.php b/app/Models/Dealer.php index 792e7ca..084a9b3 100755 --- a/app/Models/Dealer.php +++ b/app/Models/Dealer.php @@ -48,4 +48,38 @@ class Dealer extends Model ->withPivot('quantity') ->withTimestamps(); } + + /** + * Get all work prices for this dealer + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function workPrices() + { + return $this->hasMany(WorkDealerPrice::class); + } + + /** + * Get price for specific work + * + * @param int $workId + * @return WorkDealerPrice|null + */ + public function getPriceForWork($workId) + { + return $this->workPrices() + ->where('work_id', $workId) + ->active() + ->first(); + } + + /** + * Get all active work prices for this dealer + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function activeWorkPrices() + { + return $this->hasMany(WorkDealerPrice::class)->active(); + } } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index f06f568..37c604a 100755 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -10,7 +10,12 @@ class Transaction extends Model { use HasFactory, SoftDeletes; protected $fillable = [ - "user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id" + "user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id", + "claimed_at", "claimed_by" + ]; + + protected $casts = [ + 'claimed_at' => 'datetime', ]; /** diff --git a/app/Models/Work.php b/app/Models/Work.php index 3eb6696..38390b7 100755 --- a/app/Models/Work.php +++ b/app/Models/Work.php @@ -54,4 +54,52 @@ class Work extends Model { return $this->belongsTo(Category::class); } + + /** + * Get all dealer prices for this work + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function dealerPrices() + { + return $this->hasMany(WorkDealerPrice::class); + } + + /** + * Get price for specific dealer + * + * @param int $dealerId + * @return WorkDealerPrice|null + */ + public function getPriceForDealer($dealerId) + { + return $this->dealerPrices() + ->where('dealer_id', $dealerId) + ->active() + ->first(); + } + + /** + * Get price for specific dealer (including soft deleted) + * + * @param int $dealerId + * @return WorkDealerPrice|null + */ + public function getPriceForDealerWithTrashed($dealerId) + { + return $this->dealerPrices() + ->withTrashed() + ->where('dealer_id', $dealerId) + ->first(); + } + + /** + * Get all active prices for this work + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function activeDealerPrices() + { + return $this->hasMany(WorkDealerPrice::class)->active(); + } } diff --git a/app/Models/WorkDealerPrice.php b/app/Models/WorkDealerPrice.php new file mode 100644 index 0000000..198342a --- /dev/null +++ b/app/Models/WorkDealerPrice.php @@ -0,0 +1,81 @@ + 'decimal:2', + 'is_active' => 'boolean', + ]; + + /** + * Get the work associated with the price + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function work() + { + return $this->belongsTo(Work::class); + } + + /** + * Get the dealer associated with the price + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function dealer() + { + return $this->belongsTo(Dealer::class); + } + + /** + * Scope to get only active prices + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Get formatted price with currency + * + * @return string + */ + public function getFormattedPriceAttribute() + { + return number_format($this->price, 0, ',', '.') . ' ' . $this->currency; + } + + /** + * Get price for specific work and dealer + * + * @param int $workId + * @param int $dealerId + * @return WorkDealerPrice|null + */ + public static function getPriceForWorkAndDealer($workId, $dealerId) + { + return static::where('work_id', $workId) + ->where('dealer_id', $dealerId) + ->active() + ->first(); + } +} \ No newline at end of file diff --git a/app/Services/KpiService.php b/app/Services/KpiService.php index c841fa2..c994210 100644 --- a/app/Services/KpiService.php +++ b/app/Services/KpiService.php @@ -70,7 +70,7 @@ class KpiService private function getActualWorkCount(User $user, $year, $month) { return Transaction::where('user_id', $user->id) - ->where('status', 'completed') + ->whereIn('status', [0, 1]) // pending (0) and completed (1) ->whereYear('date', $year) ->whereMonth('date', $month) ->sum('qty'); @@ -329,4 +329,125 @@ class KpiService 'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false ]; } + + /** + * Get claimed transactions count for a mechanic + * + * @param User $user + * @param int|null $year + * @param int|null $month + * @return int + */ + public function getClaimedTransactionsCount(User $user, $year = null, $month = null) + { + $year = $year ?? now()->year; + $month = $month ?? now()->month; + + return Transaction::where('claimed_by', $user->id) + ->whereNotNull('claimed_at') + ->whereYear('claimed_at', $year) + ->whereMonth('claimed_at', $month) + ->sum('qty'); + } + + /** + * Calculate KPI achievement including claimed transactions + * + * @param User $user + * @param int $year + * @param int $month + * @return KpiAchievement|null + */ + public function calculateKpiAchievementWithClaims(User $user, $year = null, $month = null) + { + $year = $year ?? now()->year; + $month = $month ?? now()->month; + + // Get current KPI target + $kpiTarget = $user->kpiTargets() + ->where('is_active', true) + ->first(); + + if (!$kpiTarget) { + Log::info("No KPI target found for user {$user->id}"); + return null; + } + + // Calculate actual value including claimed transactions + $actualValue = $this->getActualWorkCountWithClaims($user, $year, $month); + + // Calculate percentage + $achievementPercentage = $kpiTarget->target_value > 0 + ? ($actualValue / $kpiTarget->target_value) * 100 + : 0; + + // Save or update achievement + return KpiAchievement::updateOrCreate( + [ + 'user_id' => $user->id, + 'year' => $year, + 'month' => $month + ], + [ + 'kpi_target_id' => $kpiTarget->id, + 'target_value' => $kpiTarget->target_value, + 'actual_value' => $actualValue, + 'achievement_percentage' => $achievementPercentage + ] + ); + } + + /** + * Get actual work count including claimed transactions + * + * @param User $user + * @param int $year + * @param int $month + * @return int + */ + private function getActualWorkCountWithClaims(User $user, $year, $month) + { + // Get transactions created by the user (including pending and completed) + $createdTransactions = Transaction::where('user_id', $user->id) + ->whereIn('status', [0, 1]) // pending (0) and completed (1) + ->whereYear('date', $year) + ->whereMonth('date', $month) + ->sum('qty'); + + // Get transactions claimed by the user + $claimedTransactions = Transaction::where('claimed_by', $user->id) + ->whereNotNull('claimed_at') + ->whereYear('claimed_at', $year) + ->whereMonth('claimed_at', $month) + ->sum('qty'); + + return $createdTransactions + $claimedTransactions; + } + + /** + * Get KPI summary including claimed transactions + * + * @param User $user + * @return array + */ + public function getKpiSummaryWithClaims(User $user) + { + $currentYear = now()->year; + $currentMonth = now()->month; + + // Calculate current month achievement including claims + $currentAchievement = $this->calculateKpiAchievementWithClaims($user, $currentYear, $currentMonth); + + // Get current month target + $currentTarget = $user->kpiTargets() + ->where('is_active', true) + ->first(); + + return [ + 'current_achievement' => $currentAchievement, + 'current_target' => $currentTarget, + 'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0, + 'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false + ]; + } } \ No newline at end of file diff --git a/database/migrations/2025_07_07_114022_add_claimed_columns_to_transactions_table.php b/database/migrations/2025_07_07_114022_add_claimed_columns_to_transactions_table.php new file mode 100644 index 0000000..a7a8f6d --- /dev/null +++ b/database/migrations/2025_07_07_114022_add_claimed_columns_to_transactions_table.php @@ -0,0 +1,33 @@ +timestamp('claimed_at')->nullable()->after('status'); + $table->unsignedBigInteger('claimed_by')->nullable()->after('claimed_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('transactions', function (Blueprint $table) { + $table->dropColumn(['claimed_at', 'claimed_by']); + }); + } +} diff --git a/database/migrations/2025_07_07_152055_create_work_dealer_prices_table.php b/database/migrations/2025_07_07_152055_create_work_dealer_prices_table.php new file mode 100644 index 0000000..a6fb6f2 --- /dev/null +++ b/database/migrations/2025_07_07_152055_create_work_dealer_prices_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('work_id')->constrained()->onDelete('cascade'); + $table->foreignId('dealer_id')->constrained()->onDelete('cascade'); + $table->decimal('price', 15, 2)->default(0.00); + $table->string('currency', 3)->default('IDR'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['work_id', 'dealer_id']); + $table->index(['dealer_id', 'is_active']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('work_dealer_prices'); + } +} diff --git a/public/js/pages/back/master/work-prices.js b/public/js/pages/back/master/work-prices.js new file mode 100644 index 0000000..9f733ca --- /dev/null +++ b/public/js/pages/back/master/work-prices.js @@ -0,0 +1,655 @@ +"use strict"; + +// Class definition +var WorkPrices = (function () { + // Private variables + var workId; + var dealersTable; + var saveTimeout = {}; // For debouncing save requests + + // Loading overlay functions + var showLoadingOverlay = function (message) { + if ($("#loading-overlay").length === 0) { + $("body").append(` +
+
+ Loading... +
+
${message}
+
+ `); + } else { + $("#loading-overlay").show(); + $("#loading-overlay .text-light:last").text(message); + } + }; + + var hideLoadingOverlay = function () { + $("#loading-overlay").hide(); + }; + + // Private functions + var initTable = function () { + dealersTable = $("#dealers-table").DataTable({ + pageLength: -1, // Show all records + lengthChange: false, // Hide length change dropdown + searching: true, + ordering: true, + info: false, // Hide "Showing X of Y entries" + responsive: true, + dom: '<"top"f>rt<"bottom"p>', // Only show search and pagination + paging: false, // Disable pagination + }); + }; + + var initEvents = function () { + // Get work ID from URL + var pathArray = window.location.pathname.split("/"); + workId = pathArray[pathArray.length - 2]; // work/{id}/set-prices + + // Save single price with debouncing + $(document).on("click", ".save-single", function () { + var dealerId = $(this).data("dealer-id"); + + // Clear existing timeout for this dealer + if (saveTimeout[dealerId]) { + clearTimeout(saveTimeout[dealerId]); + } + + // Set new timeout to prevent rapid clicks + saveTimeout[dealerId] = setTimeout(function () { + saveSinglePrice(dealerId); + }, 300); // 300ms delay + }); + + // Delete price + $(document).on("click", ".delete-price", function () { + var priceId = $(this).data("price-id"); + var dealerId = $(this).data("dealer-id"); + deletePrice(priceId, dealerId); + }); + + // Save all prices + $("#btn-save-all").on("click", function () { + saveAllPrices(); + }); + + // Status toggle + $(document).on("change", ".status-input", function () { + var dealerId = $(this).data("dealer-id"); + var isChecked = $(this).is(":checked"); + var label = $(this).siblings("label"); + var checkbox = $(this); + + // Update visual immediately + if (isChecked) { + label.text("Aktif").removeClass("inactive").addClass("active"); + } else { + label + .text("Nonaktif") + .removeClass("active") + .addClass("inactive"); + } + + // Send AJAX request to update database + toggleStatus(dealerId, isChecked, checkbox, label); + }); + + // Format price input with thousand separator + $(document).on("input", ".price-input", function () { + var input = $(this); + var value = input.val().replace(/[^\d]/g, ""); + + if (value === "") { + input.val("0"); + } else { + var numValue = parseInt(value); + input.val(numValue.toLocaleString("id-ID")); + // Don't update original value here - let it be updated only when saving + } + }); + + // Format price inputs on page load + $(".price-input").each(function () { + var input = $(this); + var value = input.val(); + if (value && value !== "0") { + var numValue = parseInt(value.replace(/[^\d]/g, "")); + input.val(numValue.toLocaleString("id-ID")); + // Store the original numeric value for comparison + input.data("original-value", numValue.toString()); + console.log( + "Initialized price for dealer", + input.attr("name").replace("price_", ""), + ":", + numValue + ); + } + }); + }; + + var saveSinglePrice = function (dealerId) { + // Prevent multiple clicks + var saveButton = $('.save-single[data-dealer-id="' + dealerId + '"]'); + if (saveButton.hasClass("loading")) { + return; // Already processing + } + + var priceInput = $('input[name="price_' + dealerId + '"]'); + var statusInput = $('input[name="status_' + dealerId + '"]'); + var price = priceInput.val().replace(/[^\d]/g, ""); // Remove non-numeric characters + var isActive = statusInput.is(":checked"); + + if (!price || parseInt(price) <= 0) { + toastr.error("Harga harus lebih dari 0"); + return; + } + + // Get original price from data attribute (without separator) + var originalPrice = priceInput.data("original-value") || "0"; + var currentPrice = parseInt(price); + var originalPriceInt = parseInt(originalPrice); + + console.log( + "Debug - Original price:", + originalPriceInt, + "Current price:", + currentPrice + ); + + // If price hasn't actually changed, don't update + if (currentPrice === originalPriceInt && originalPrice !== "0") { + toastr.info("Harga tidak berubah, tidak perlu update"); + return; + } + + // If price has changed, update original value for next comparison + if (currentPrice !== originalPriceInt) { + priceInput.data("original-value", currentPrice.toString()); + console.log( + "Price changed from", + originalPriceInt, + "to", + currentPrice + ); + } + + // Disable button and show loading state + saveButton.addClass("loading").prop("disabled", true); + var originalText = saveButton.text(); + saveButton.text("Menyimpan..."); + + var data = { + work_id: parseInt(workId), + dealer_id: parseInt(dealerId), + price: currentPrice, // Use the validated price + currency: "IDR", + is_active: isActive ? 1 : 0, + }; + + // Debug: Log the data being sent + console.log("Sending data:", data); + console.log("Original price:", originalPriceInt); + console.log("Current price:", currentPrice); + + $.ajax({ + url: "/admin/work/" + workId + "/prices", + method: "POST", + data: data, + headers: { + "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"), + }, + beforeSend: function () { + console.log("Sending AJAX request with data:", data); + }, + success: function (response) { + // Re-enable button + saveButton + .removeClass("loading") + .prop("disabled", false) + .text(originalText); + + if (response.status === 200) { + toastr.success(response.message); + // Update UI + updateRowAfterSave(dealerId, response.data); + + // Ensure consistent formatting after update + var updatedPrice = priceInput.val().replace(/[^\d]/g, ""); + if (updatedPrice && updatedPrice !== "0") { + var formattedPrice = + parseInt(updatedPrice).toLocaleString("id-ID"); + priceInput.val(formattedPrice); + } + + // Show brief loading message + toastr.info( + "Data berhasil disimpan, memperbarui tampilan..." + ); + } else { + toastr.error(response.message || "Terjadi kesalahan"); + } + }, + error: function (xhr) { + // Re-enable button + saveButton + .removeClass("loading") + .prop("disabled", false) + .text(originalText); + + var message = "Terjadi kesalahan"; + if (xhr.responseJSON) { + if (xhr.responseJSON.message) { + message = xhr.responseJSON.message; + } + if (xhr.responseJSON.errors) { + // Show validation errors + var errorMessages = []; + for (var field in xhr.responseJSON.errors) { + var fieldName = field; + switch (field) { + case "work_id": + fieldName = "ID Pekerjaan"; + break; + case "dealer_id": + fieldName = "ID Dealer"; + break; + case "price": + fieldName = "Harga"; + break; + case "currency": + fieldName = "Mata Uang"; + break; + case "is_active": + fieldName = "Status Aktif"; + break; + } + errorMessages.push( + fieldName + + ": " + + xhr.responseJSON.errors[field][0] + ); + } + message = errorMessages.join("\n"); + } + } + toastr.error(message); + }, + }); + }; + + var saveAllPrices = function () { + // Prevent multiple clicks + var saveAllButton = $("#btn-save-all"); + if (saveAllButton.hasClass("loading")) { + return; // Already processing + } + + var prices = []; + var hasValidPrice = false; + + $(".price-input").each(function () { + var dealerId = $(this).attr("name").replace("price_", ""); + var price = $(this).val().replace(/[^\d]/g, ""); // Remove non-numeric characters + var statusInput = $('input[name="status_' + dealerId + '"]'); + var isActive = statusInput.is(":checked"); + + if (price && parseInt(price) > 0) { + hasValidPrice = true; + prices.push({ + dealer_id: dealerId, + price: parseInt(price), + currency: "IDR", + is_active: isActive, + }); + } + }); + + if (!hasValidPrice) { + toastr.error("Minimal satu dealer harus memiliki harga yang valid"); + return; + } + + // Disable button and show loading state + saveAllButton.addClass("loading").prop("disabled", true); + var originalText = saveAllButton.text(); + saveAllButton.text("Menyimpan..."); + + // Show confirmation + $("#confirmMessage").text( + "Apakah Anda yakin ingin menyimpan semua harga?" + ); + $("#confirmModal").modal("show"); + + $("#confirmAction") + .off("click") + .on("click", function () { + $("#confirmModal").modal("hide"); + + $.ajax({ + url: "/admin/work/" + workId + "/prices/bulk", + method: "POST", + data: { + prices: prices, + }, + headers: { + "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr( + "content" + ), + }, + success: function (response) { + // Re-enable button + saveAllButton + .removeClass("loading") + .prop("disabled", false) + .text(originalText); + + if (response.status === 200) { + toastr.success(response.message); + + // Show loading overlay + showLoadingOverlay("Memperbarui data..."); + + // Reload page to update all data + setTimeout(function () { + location.reload(); + }, 1000); + } else { + toastr.error( + response.message || "Terjadi kesalahan" + ); + } + }, + error: function (xhr) { + // Re-enable button + saveAllButton + .removeClass("loading") + .prop("disabled", false) + .text(originalText); + + var message = "Terjadi kesalahan"; + if (xhr.responseJSON && xhr.responseJSON.message) { + message = xhr.responseJSON.message; + } + toastr.error(message); + }, + }); + }); + }; + + var deletePrice = function (priceId, dealerId) { + $("#confirmMessage").text( + "Apakah Anda yakin ingin menghapus harga ini? Harga yang dihapus dapat dipulihkan dengan menyimpan ulang." + ); + $("#confirmModal").modal("show"); + + $("#confirmAction") + .off("click") + .on("click", function () { + $("#confirmModal").modal("hide"); + + // Show loading overlay + showLoadingOverlay("Menghapus harga..."); + + $.ajax({ + url: "/admin/work/" + workId + "/prices/" + priceId, + method: "DELETE", + headers: { + "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr( + "content" + ), + }, + success: function (response) { + hideLoadingOverlay(); + + if (response.status === 200) { + toastr.success(response.message); + // Reset the row + resetRow(dealerId); + } else { + toastr.error( + response.message || "Terjadi kesalahan" + ); + } + }, + error: function (xhr) { + hideLoadingOverlay(); + + var message = "Terjadi kesalahan"; + if (xhr.responseJSON && xhr.responseJSON.message) { + message = xhr.responseJSON.message; + } + toastr.error(message); + }, + }); + }); + }; + + // Handle delete button click + $(document).on("click", ".delete-price", function () { + var priceId = $(this).data("price-id"); + var dealerId = $(this).data("dealer-id"); + deletePrice(priceId, dealerId); + }); + + var updateRowAfterSave = function (dealerId, data) { + var row = $('[data-dealer-id="' + dealerId + '"]'); + var priceInput = row.find('input[name="price_' + dealerId + '"]'); + var statusInput = row.find('input[name="status_' + dealerId + '"]'); + var label = statusInput.siblings("label").find(".status-text"); + var actionCell = row.find("td:last"); + + // Update price input if data contains price + if (data.price !== undefined) { + // Only update if the price actually changed + var currentDisplayValue = priceInput.val().replace(/[^\d]/g, ""); + var newPrice = parseInt(data.price); + + if (parseInt(currentDisplayValue) !== newPrice) { + priceInput.val(newPrice.toLocaleString("id-ID")); + // Update the original value for future comparisons + priceInput.data("original-value", newPrice.toString()); + } + } + + // If this is a new record (price = 0), update the save button + if (data.price === 0) { + actionCell + .find(".save-single") + .text("Simpan") + .removeClass("btn-warning") + .addClass("btn-success"); + } + + // Update status if data contains is_active + if (data.is_active !== undefined) { + statusInput.prop("checked", data.is_active); + if (data.is_active) { + label.text("Aktif").removeClass("inactive").addClass("active"); + } else { + label + .text("Nonaktif") + .removeClass("active") + .addClass("inactive"); + } + } + + // Update save button if this is a new price save (not just status toggle) + if (data.price !== undefined) { + actionCell + .find(".save-single") + .text("Update") + .removeClass("btn-success") + .addClass("btn-warning"); + + // Update delete button + if (actionCell.find(".delete-price").length === 0) { + // Add delete button if it doesn't exist + var deleteBtn = + ''; + actionCell.find(".d-flex.flex-row.gap-1").append(deleteBtn); + } else { + // Update existing delete button with new price ID + actionCell.find(".delete-price").attr("data-price-id", data.id); + } + } + }; + + var toggleStatus = function (dealerId, isActive, checkbox, label) { + var data = { + dealer_id: parseInt(dealerId), + is_active: isActive, + }; + + $.ajax({ + url: "/admin/work/" + workId + "/prices/toggle-status", + method: "POST", + data: data, + headers: { + "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"), + }, + beforeSend: function () { + // Show brief loading indicator on checkbox + checkbox.prop("disabled", true); + }, + success: function (response) { + // Re-enable checkbox + checkbox.prop("disabled", false); + + if (response.status === 200) { + toastr.success(response.message); + + // Update UI if needed + if (response.data) { + // If this is a new record, update the row to show save button + if (response.data.price === 0) { + updateRowAfterSave(dealerId, response.data); + } + } + } else { + toastr.error(response.message || "Terjadi kesalahan"); + // Revert checkbox state + checkbox.prop("checked", !isActive); + if (!isActive) { + label.text("Aktif"); + } else { + label.text("Nonaktif"); + } + } + }, + error: function (xhr) { + // Re-enable checkbox + checkbox.prop("disabled", false); + + var message = "Terjadi kesalahan"; + if (xhr.responseJSON) { + if (xhr.responseJSON.message) { + message = xhr.responseJSON.message; + } + if (xhr.responseJSON.errors) { + var errorMessages = []; + for (var field in xhr.responseJSON.errors) { + var fieldName = field; + switch (field) { + case "dealer_id": + fieldName = "ID Dealer"; + break; + case "is_active": + fieldName = "Status Aktif"; + break; + } + errorMessages.push( + fieldName + + ": " + + xhr.responseJSON.errors[field][0] + ); + } + message = errorMessages.join("\n"); + } + } + toastr.error(message); + + // Revert checkbox state + checkbox.prop("checked", !isActive); + if (!isActive) { + label.text("Aktif"); + } else { + label.text("Nonaktif"); + } + }, + }); + }; + + var resetRow = function (dealerId) { + var row = $('[data-dealer-id="' + dealerId + '"]'); + var priceInput = row.find('input[name="price_' + dealerId + '"]'); + var statusInput = row.find('input[name="status_' + dealerId + '"]'); + var label = statusInput.siblings("label").find(".status-text"); + var actionCell = row.find("td:last"); + + // Reset price input + priceInput.val("0"); + + // Reset status + statusInput.prop("checked", false); + label.text("Nonaktif").removeClass("active").addClass("inactive"); + + // Remove delete button and update save button + actionCell.find(".delete-price").remove(); + actionCell + .find(".save-single") + .text("Simpan") + .removeClass("btn-warning") + .addClass("btn-success"); + }; + + // Public methods + return { + init: function () { + initTable(); + initEvents(); + // Initialize price formatting on page load + setTimeout(function () { + $(".price-input").each(function () { + var value = $(this).val(); + if (value && value !== "0") { + var numValue = parseInt(value.replace(/[^\d]/g, "")); + if (!isNaN(numValue)) { + $(this).val(numValue.toLocaleString("id-ID")); + } + } + }); + }, 100); + + // Cleanup timeouts on page unload + $(window).on("beforeunload", function () { + for (var dealerId in saveTimeout) { + if (saveTimeout[dealerId]) { + clearTimeout(saveTimeout[dealerId]); + } + } + }); + }, + }; +})(); + +// On document ready +jQuery(document).ready(function () { + WorkPrices.init(); +}); diff --git a/resources/views/back/master/work_prices.blade.php b/resources/views/back/master/work_prices.blade.php new file mode 100644 index 0000000..d13fcab --- /dev/null +++ b/resources/views/back/master/work_prices.blade.php @@ -0,0 +1,322 @@ +@extends('layouts.backapp') + +@section('content') + + + +
+
+
+ + + +

+ Set Harga Pekerjaan: {{ $work->name }} +

+
+
+
+
+ +
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + @foreach($dealers as $index => $dealer) + @php + $existingPrice = $work->getPriceForDealer($dealer->id); + @endphp + + + + + + + + + @endforeach + +
NoNama DealerKode DealerHarga (IDR)StatusAksi
{{ $index + 1 }}{{ $dealer->name }}{{ $dealer->dealer_code }} +
+
+ Rp +
+ +
+
+ is_active ? 'checked' : '' }}> + + +
+ + @if($existingPrice) + + @endif +
+
+ +
+ + @if($dealers->count() == 0) +
+
+
+ + Tidak ada dealer yang tersedia. +
+
+
+ @endif +
+
+ + + + + +@endsection + +@section('javascripts') + +@endsection \ No newline at end of file diff --git a/resources/views/back/users.blade.php b/resources/views/back/users.blade.php index 58038db..606e663 100755 --- a/resources/views/back/users.blade.php +++ b/resources/views/back/users.blade.php @@ -24,22 +24,24 @@
- - - - - - - - - - - - - - -
NoDealerRoleNama PenggunaEmailAksi
- +
+ + + + + + + + + + + + + + +
NoDealerRoleNama PenggunaEmailAksi
+ +
diff --git a/resources/views/layouts/partials/sidebarMenu.blade.php b/resources/views/layouts/partials/sidebarMenu.blade.php index bc86027..c30f916 100755 --- a/resources/views/layouts/partials/sidebarMenu.blade.php +++ b/resources/views/layouts/partials/sidebarMenu.blade.php @@ -48,7 +48,7 @@ @if(Gate::check('view', $menus['user.index']) || Gate::check('view', $menus['roleprivileges.index']))
- + {{-- --}} Manajemen Pengguna
@@ -77,7 +77,7 @@ @if(Gate::check('view', $menus['work.index']) || Gate::check('view', $menus['category.index']) || Gate::check('view', $menus['dealer.index']))
- + {{-- --}} Manajemen Transaksi
@@ -115,7 +115,7 @@ @if(Gate::check('view', $menus['products.index']) || Gate::check('view', $menus['product_categories.index']) || Gate::check('view', $menus['mutations.index']) || Gate::check('view', $menus['opnames.index']) || Gate::check('view', $menus['stock-audit.index']))
- + {{-- --}} Manajemen Gudang
@@ -171,7 +171,7 @@ @if(Gate::check('view', $menus['report.transaction_sa']) || Gate::check('view', $menus['report.transaction']) || Gate::check('view', $menus['report.transaction_dealer']) || Gate::check('view', $menus['work.index']))
- + {{-- --}} Laporan
@@ -223,13 +223,17 @@ @endcan + {{-- Section Header - Only show if user has access to any submenu --}} + @if(Gate::check('view', $menus['kpi.targets.index']))
- + {{-- --}} KPI
+ @endif + {{-- Submenu Items --}} @can('view', $menus['kpi.targets.index'])
  • diff --git a/resources/views/transaction/index.blade.php b/resources/views/transaction/index.blade.php index 98366b3..048ec98 100755 --- a/resources/views/transaction/index.blade.php +++ b/resources/views/transaction/index.blade.php @@ -321,6 +321,186 @@ use Illuminate\Support\Facades\Auth; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } + /* Claim tab specific styling */ + #claimTransactionsTable { + font-size: 14px; + } + + #claimTransactionsTable th { + background-color: #f8f9fa; + font-weight: 600; + text-align: center; + vertical-align: middle; + border: 1px solid #dee2e6; + } + + #claimTransactionsTable td { + vertical-align: middle; + border: 1px solid #dee2e6; + } + + #claimTransactionsTable .btn { + font-size: 12px; + padding: 4px 8px; + margin: 1px; + } + + /* Status badge styling for claim table */ + #claimTransactionsTable .badge { + font-size: 11px; + padding: 4px 8px; + border-radius: 12px; + font-weight: 600; + } + + /* Tab claim specific styling */ + #form-claim { + background-color: #fff; + padding: 20px; + border: 1px solid #dee2e6; + border-top: none; + border-radius: 0 0 5px 5px; + } + + #form-claim h6 { + color: #495057; + font-weight: 600; + margin-bottom: 15px; + } + + /* Stock tab specific styling - simplified */ + + /* Ensure stock sub-tabs are visible */ + #stock .nav-tabs { + display: flex; + border-bottom: 1px solid #dee2e6; + margin-top: 1rem; + } + + #stock .nav-tabs .nav-link { + padding: 0.5rem 1rem; + border: 1px solid transparent; + border-radius: 0.25rem 0.25rem 0 0; + text-decoration: none; + color: #495057; + cursor: pointer; + } + + #stock .nav-tabs .nav-link.active { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; + } + + /* Ensure transaksi sub-tabs have the same styling */ + #transaksi .nav-tabs { + display: flex; + border-bottom: 1px solid #dee2e6; + margin-top: 1rem; + } + + #transaksi .nav-tabs .nav-link { + padding: 0.5rem 1rem; + border: 1px solid transparent; + border-radius: 0.25rem 0.25rem 0 0; + text-decoration: none; + color: #495057; + cursor: pointer; + } + + #transaksi .nav-tabs .nav-link.active { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; + } + + /* Prevent stock tabs from appearing in transaksi tab */ + #transaksi #stock, + #transaksi #opname, + #transaksi #mutasi, + #transaksi #penerimaan { + display: none !important; + } + + /* Prevent transaksi tabs from appearing in stock tab */ + #stock #form-kerja, + #stock #form-cuci, + #stock #form-claim { + display: none !important; + } + + /* Ensure proper tab display */ + .tab-content > .tab-pane { + display: none; + } + + .tab-content > .tab-pane.active { + display: block; + } + + /* Ensure main tab content is properly separated */ + #transaksi { + display: none; + } + + #transaksi.active { + display: block; + } + + #stock { + display: none; + } + + #stock.active { + display: block; + } + + /* Ensure sub-tab content is properly contained within their parent tabs */ + #transaksi .tab-content .tab-pane { + display: none; + } + + #transaksi .tab-content .tab-pane.active { + display: block; + } + + #stock .tab-content .tab-pane { + display: none; + } + + #stock .tab-content .tab-pane.active { + display: block; + } + + /* Additional safety to prevent cross-tab content display */ + #transaksi .tab-content #opname, + #transaksi .tab-content #mutasi, + #transaksi .tab-content #penerimaan { + display: none !important; + } + + #stock .tab-content #form-kerja, + #stock .tab-content #form-cuci, + #stock .tab-content #form-claim { + display: none !important; + } + + /* Force proper tab separation */ + .tab-content > #stock { + display: none !important; + } + + .tab-content > #stock.active { + display: block !important; + } + + .tab-content > #transaksi { + display: none !important; + } + + .tab-content > #transaksi.active { + display: block !important; + } @endsection @@ -368,17 +548,17 @@ use Illuminate\Support\Facades\Auth;
    Target {{ $kpiData['period'] }}
    - {{ number_format($kpiData['target']) }} Pekerjaan + {{ number_format($kpiData['target']) }} Pekerjaan
    Pencapaian
    - {{ number_format($kpiData['actual']) }} Pekerjaan + {{ number_format($kpiData['actual']) }} Pekerjaan
    Progress - {{ $kpiData['percentage'] }}% + {{ $kpiData['percentage'] }}%
    @php @@ -406,6 +586,7 @@ use Illuminate\Support\Facades\Auth; @endif
    + @else
    @@ -442,10 +623,10 @@ use Illuminate\Support\Facades\Auth; @@ -455,336 +636,368 @@ use Illuminate\Support\Facades\Auth;
    - -
    -
    - @csrf - - - - +
    + + + @csrf + + + + - - @if($errors->has('error')) - - @endif -
    -
    - - - - @if(old('form') == 'work') - @error('spk_no') - - {!! $message !!} - - @enderror + + @if($errors->has('error')) + @endif -
    -
    - - - @if(old('form') == 'work') - @error('police_number') - - {!! $message !!} - - @enderror - @endif -
    -
    -
    -
    - - - @if(old('form') == 'work') - @error('warranty') - - {!! $message !!} - - @enderror - @endif -
    -
    - - - @if(old('form') == 'work') - @error('date') - - {!! $message !!} - - @enderror - @endif -
    -
    -
    - - - @error('user_sa_id') - - {!! $message !!} - - @enderror -
    - -
    - @if (old('work_id') && old('form') == 'work') - {{-- @php - dd($errors->all()); - @endphp --}} - - @for ($i = 0; $i < count(old('work_id')); $i++) -
    -
    - - - @error('work_id.'.$i) - - {!! $message !!} - - @enderror +
    +
    + + + + @if(old('form') == 'work') + @error('spk_no') + + {!! $message !!} + + @enderror + @endif +
    +
    + + + @if(old('form') == 'work') + @error('police_number') + + {!! $message !!} + + @enderror + @endif +
    +
    +
    +
    + + + @if(old('form') == 'work') + @error('warranty') + + {!! $message !!} + + @enderror + @endif +
    +
    + + + @if(old('form') == 'work') + @error('date') + + {!! $message !!} + + @enderror + @endif +
    +
    +
    + + + @error('user_sa_id') + + {!! $message !!} + + @enderror +
    + +
    + @if (old('work_id') && old('form') == 'work') + {{-- @php + dd($errors->all()); + @endphp --}} + + @for ($i = 0; $i < count(old('work_id')); $i++) +
    +
    + + + @error('work_id.'.$i) + + {!! $message !!} + + @enderror +
    +
    + + @error('quantity.'.$i) + + {!! $message !!} + + @enderror +
    +
    + +
    +
    + @endfor + @else + +
    +
    + +
    +
    + +
    +
    + +
    -
    - - @error('quantity.'.$i) - - {!! $message !!} - - @enderror +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + @endif +
    +
    - +
    - @endfor - @else - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    - -
    + +
    -
    -
    - -
    -
    - -
    -
    - -
    + + +
    +
    + @csrf + + + +
    +
    + + + @if(old('form') == 'wash') + @error('spk_no') + + {!! $message !!} + + @enderror + @endif
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - @endif -
    -
    -
    - +
    + + + @if(old('form') == 'wash') + @error('police_number') + + {!! $message !!} + + @enderror + @endif
    -
    - - -
    - - -
    -
    - @csrf - - - -
    -
    - - - @if(old('form') == 'wash') - @error('spk_no') - - {!! $message !!} - - @enderror - @endif +
    +
    + + + @if(old('form') == 'wash') + @error('warranty') + + {!! $message !!} + + @enderror + @endif +
    +
    + + + @if(old('form') == 'wash') + @error('date') + + {!! $message !!} + + @enderror + @endif +
    -
    - - - @if(old('form') == 'wash') - @error('police_number') - - {!! $message !!} - - @enderror - @endif -
    -
    -
    -
    - - + + @foreach ($user_sas as $user_sa) + + @endforeach @if(old('form') == 'wash') - @error('warranty') + @error('user_sa_id') {!! $message !!} @enderror @endif
    -
    - - - @if(old('form') == 'wash') - @error('date') - - {!! $message !!} - - @enderror - @endif + +
    + + + + +
    + + +
    + + + @if(Auth::user()->role_id == 3) +
    +
    +
    Daftar Pekerjaan yang Dapat Diklaim
    +
    + + + + + + + + + + + + + + + + +
    TanggalNo. SPKNo. PolisiPekerjaanQtyService AdvisorStatusAksi
    +
    +
    -
    -
    - - - @if(old('form') == 'wash') - @error('user_sa_id') - - {!! $message !!} - - @enderror @endif
    - -
    - - - - -
    - - -
    - - +
    - -