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 .= ''; + $buttons .= 'Klaim'; + $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 .= ' Edit @@ -157,4 +164,20 @@ class WorkController extends Controller return response()->json($response); } + + /** + * Show the form for setting prices per dealer for a specific work. + * + * @param \App\Models\Work $work + * @return \Illuminate\Http\Response + */ + public function showPrices(Work $work) + { + $menu = Menu::where('link', 'work.index')->first(); + abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User'); + + $dealers = \App\Models\Dealer::all(); + + return view('back.master.work_prices', compact('work', 'dealers')); + } } diff --git a/app/Http/Controllers/WorkDealerPriceController.php b/app/Http/Controllers/WorkDealerPriceController.php new file mode 100644 index 0000000..632b229 --- /dev/null +++ b/app/Http/Controllers/WorkDealerPriceController.php @@ -0,0 +1,363 @@ +ajax()) { + $data = WorkDealerPrice::with(['dealer']) + ->where('work_id', $work->id) + ->select('work_dealer_prices.*'); + + return DataTables::of($data) + ->addIndexColumn() + ->addColumn('dealer_name', function($row) { + return $row->dealer->name; + }) + ->addColumn('formatted_price', function($row) { + return $row->formatted_price; + }) + ->addColumn('action', function($row) { + $btn = ''; + $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 = + 'Hapus'; + 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 }} + + + + + + + Simpan Semua Harga + + + + + + + + + + + + + + No + Nama Dealer + Kode Dealer + Harga (IDR) + Status + Aksi + + + + @foreach($dealers as $index => $dealer) + @php + $existingPrice = $work->getPriceForDealer($dealer->id); + @endphp + + {{ $index + 1 }} + {{ $dealer->name }} + {{ $dealer->dealer_code }} + + + + Rp + + + + + + is_active ? 'checked' : '' }}> + Aktif + + + + + {{ $existingPrice ? 'Update' : 'Simpan' }} + + @if($existingPrice) + + Hapus + + @endif + + + + @endforeach + + + + + + @if($dealers->count() == 0) + + + + + Tidak ada dealer yang tersedia. + + + + @endif + + + + + + + + + + + Konfirmasi + + + × + + + + + + + + + + + +@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 @@ - - - - - No - Dealer - Role - Nama Pengguna - Email - Aksi - - - - - - + + + + + + No + Dealer + Role + Nama Pengguna + Email + Aksi + + + + + + + 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; - Transaksi + Transaksi - Stock + Stock @@ -455,336 +636,368 @@ use Illuminate\Support\Facades\Auth; - Form Kerja + Form Kerja - Form Cuci + Form Cuci + @if(Auth::user()->role_id == 3) + + Klaim + + @endif - - - - @csrf - - - - + + + + @csrf + + + + - - @if($errors->has('error')) - - Error: - {{ $errors->first('error') }} - - × - - - @endif - - - No. SPK - - - @if(old('form') == 'work') - @error('spk_no') - - {!! $message !!} - - @enderror + + @if($errors->has('error')) + + Error: + {{ $errors->first('error') }} + + × + + @endif - - - No. Polisi - - @if(old('form') == 'work') - @error('police_number') - - {!! $message !!} - - @enderror - @endif - - - - - Warranty - - Warranty - Ya - Tidak - - @if(old('form') == 'work') - @error('warranty') - - {!! $message !!} - - @enderror - @endif - - - Tanggal Pekerjaan - - @if(old('form') == 'work') - @error('date') - - {!! $message !!} - - @enderror - @endif - - - - Service Advisor - - Service Advisor - @foreach ($user_sas as $user_sa) - id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }} - @endforeach - - @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++) - - - - Pekerjaan - @foreach ($work_works as $work) - id == old('work_id.'.$i)) selected @endif>{{ $work->name }} - @endforeach - - - @error('work_id.'.$i) - - {!! $message !!} - - @enderror + + + No. SPK + + + @if(old('form') == 'work') + @error('spk_no') + + {!! $message !!} + + @enderror + @endif + + + No. Polisi + + @if(old('form') == 'work') + @error('police_number') + + {!! $message !!} + + @enderror + @endif + + + + + Warranty + + Warranty + Ya + Tidak + + @if(old('form') == 'work') + @error('warranty') + + {!! $message !!} + + @enderror + @endif + + + Tanggal Pekerjaan + + @if(old('form') == 'work') + @error('date') + + {!! $message !!} + + @enderror + @endif + + + + Service Advisor + + Service Advisor + @foreach ($user_sas as $user_sa) + id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }} + @endforeach + + @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++) + + + + Pekerjaan + @foreach ($work_works as $work) + id == old('work_id.'.$i)) selected @endif>{{ $work->name }} + @endforeach + + + @error('work_id.'.$i) + + {!! $message !!} + + @enderror + + + + @error('quantity.'.$i) + + {!! $message !!} + + @enderror + + + X + + + @endfor + @else + + + + + Pekerjaan + @foreach ($work_works as $work) + {{ $work->name }} + @endforeach + + + + + + + X + - - - @error('quantity.'.$i) - - {!! $message !!} - - @enderror + + + + Pekerjaan + @foreach ($work_works as $work) + {{ $work->name }} + @endforeach + + + + + + + X + + + + + Pekerjaan + @foreach ($work_works as $work) + {{ $work->name }} + @endforeach + + + + + + + X + + + + + + Pekerjaan + @foreach ($work_works as $work) + {{ $work->name }} + @endforeach + + + + + + + X + + + + + + Pekerjaan + @foreach ($work_works as $work) + {{ $work->name }} + @endforeach + + + + + + + X + + + @endif + + - X + + - @endfor - @else - - - - - Pekerjaan - @foreach ($work_works as $work) - {{ $work->name }} - @endforeach - - - - - - - X - - - - - Pekerjaan - @foreach ($work_works as $work) - {{ $work->name }} - @endforeach - - - - - - - X - + Simpan + - - - - Pekerjaan - @foreach ($work_works as $work) - {{ $work->name }} - @endforeach - - - - - - - X - + + + + + @csrf + + + + + + No. SPK + + @if(old('form') == 'wash') + @error('spk_no') + + {!! $message !!} + + @enderror + @endif - - - - Pekerjaan - @foreach ($work_works as $work) - {{ $work->name }} - @endforeach - - - - - - - X - - - - - - Pekerjaan - @foreach ($work_works as $work) - {{ $work->name }} - @endforeach - - - - - - - X - - - @endif - - - - + + + No. Polisi + + @if(old('form') == 'wash') + @error('police_number') + + {!! $message !!} + + @enderror + @endif - - Simpan - - - - - - - @csrf - - - - - - No. SPK - - @if(old('form') == 'wash') - @error('spk_no') - - {!! $message !!} - - @enderror - @endif + + + Warranty + + Warranty + Ya + Tidak + + @if(old('form') == 'wash') + @error('warranty') + + {!! $message !!} + + @enderror + @endif + + + Tanggal Pekerjaan + + @if(old('form') == 'wash') + @error('date') + + {!! $message !!} + + @enderror + @endif + - - No. Polisi - - @if(old('form') == 'wash') - @error('police_number') - - {!! $message !!} - - @enderror - @endif - - - - - Warranty - - Warranty - Ya - Tidak + + Service Advisor + + Service Advisor + @foreach ($user_sas as $user_sa) + id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }} + @endforeach @if(old('form') == 'wash') - @error('warranty') + @error('user_sa_id') {!! $message !!} @enderror @endif - - Tanggal Pekerjaan - - @if(old('form') == 'wash') - @error('date') - - {!! $message !!} - - @enderror - @endif + + + Pekerjaan + + + + + Simpan + + + + + @if(Auth::user()->role_id == 3) + + + Daftar Pekerjaan yang Dapat Diklaim + + + + + Tanggal + No. SPK + No. Polisi + Pekerjaan + Qty + Service Advisor + Status + Aksi + + + + + + + + - - - Service Advisor - - Service Advisor - @foreach ($user_sas as $user_sa) - id == old('user_sa_id')) selected @enderror @endif value="{{ $user_sa->id }}">{{ $user_sa->name }} - @endforeach - - @if(old('form') == 'wash') - @error('user_sa_id') - - {!! $message !!} - - @enderror @endif - - - Pekerjaan - - - - - Simpan - - - - + - - + + - Opname + Opname - Mutasi + Mutasi - Penerimaan Mutasi + Penerimaan Mutasi - - + @csrf @@ -846,7 +1059,7 @@ use Illuminate\Support\Facades\Auth; - + @csrf @@ -931,7 +1144,7 @@ use Illuminate\Support\Facades\Auth; - + Daftar Mutasi yang Perlu Diterima & Disetujui @@ -956,7 +1169,7 @@ use Illuminate\Support\Facades\Auth; - + @@ -1110,6 +1323,14 @@ use Illuminate\Support\Facades\Auth; // Setup form fields without stock checking $(document).ready(function() { + // Ensure transaksi tab is active by default if no tab is active + if (!$('.nav-tabs-line-primary .nav-link.active').length) { + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + $('.nav-link[href="#form-kerja"]').addClass('active'); + $('#form-kerja').addClass('active'); + } + // Ensure placeholder options are properly disabled and not selectable $('select[name="work_id[]"]').each(function() { var $select = $(this); @@ -1385,8 +1606,7 @@ use Illuminate\Support\Facades\Auth; $("#opnameForm").submit(function(e) { e.preventDefault(); - // Save current tab to localStorage - localStorage.setItem('activeTab', '#opname'); + // Set default values for empty fields and validate var hasValidStock = false; @@ -1490,8 +1710,7 @@ use Illuminate\Support\Facades\Auth; $(document).on('submit', '#mutasiForm', function(e) { e.preventDefault(); - // Save current tab to localStorage - localStorage.setItem('activeTab', '#mutasi'); + // Validate form var isValid = true; @@ -1778,8 +1997,10 @@ use Illuminate\Support\Facades\Auth; $('#date-opname').val(today); } - // Initialize opname form - updateProductCounter(); + // Initialize opname form only if opname tab is active + if ($('#opname').hasClass('active')) { + updateProductCounter(); + } // Initialize mutasi form updateRemoveButtonsMutasi(); @@ -1787,42 +2008,61 @@ use Illuminate\Support\Facades\Auth; // Initialize select2 for mutasi form initMutasiSelect2(); + // Initialize claim table if claim tab is active (only for mechanics) + if ($('#form-claim').hasClass('active') && {{ Auth::user()->role_id }} == 3) { + setTimeout(function() { + initClaimTransactionsTable(); + }, 100); + } + + + // Check if we should show specific tab (after form submission) @if(session('success') || session('error') || $errors->any()) @if(session('active_tab') == 'opname') // Activate stock tab and opname sub-tab - $('.nav-link[href="#stock"]').tab('show'); - setTimeout(function() { - $('.nav-link[href="#opname"]').tab('show'); - }, 100); + $('.nav-link[href="#stock"]').addClass('active'); + $('#stock').addClass('active'); + $('.nav-link[href="#opname"]').addClass('active'); + $('#opname').addClass('active'); + updateProductCounter(); @elseif(session('active_tab') == 'mutasi') // Activate stock tab and mutasi sub-tab - $('.nav-link[href="#stock"]').tab('show'); + $('.nav-link[href="#stock"]').addClass('active'); + $('#stock').addClass('active'); + $('.nav-link[href="#mutasi"]').addClass('active'); + $('#mutasi').addClass('active'); setTimeout(function() { - $('.nav-link[href="#mutasi"]').tab('show'); + initMutasiSelect2(); + updateRemoveButtonsMutasi(); }, 100); @elseif(session('active_tab') == 'penerimaan') // Activate stock tab and penerimaan sub-tab - $('.nav-link[href="#stock"]').tab('show'); + $('.nav-link[href="#stock"]').addClass('active'); + $('#stock').addClass('active'); + $('.nav-link[href="#penerimaan"]').addClass('active'); + $('#penerimaan').addClass('active'); setTimeout(function() { - $('.nav-link[href="#penerimaan"]').tab('show'); - // Initialize table after tab is shown - setTimeout(function() { - initReceiveMutationsTable(); - }, 200); + initReceiveMutationsTable(); }, 100); @elseif($errors->any() && old('form') == 'work') // Activate transaksi tab and form kerja sub-tab when there are work form errors - $('.nav-link[href="#transaksi"]').tab('show'); - setTimeout(function() { - $('.nav-link[href="#form-kerja"]').tab('show'); - }, 100); + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + $('.nav-link[href="#form-kerja"]').addClass('active'); + $('#form-kerja').addClass('active'); @elseif($errors->any() && old('form') == 'wash') // Activate transaksi tab and form cuci sub-tab when there are wash form errors - $('.nav-link[href="#transaksi"]').tab('show'); - setTimeout(function() { - $('.nav-link[href="#form-cuci"]').tab('show'); - }, 100); + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + $('.nav-link[href="#form-cuci"]').addClass('active'); + $('#form-cuci').addClass('active'); + @elseif($errors->any() && old('form') == 'claim' && Auth::user()->role_id == 3) + // Activate transaksi tab and form claim sub-tab when there are claim form errors (only for mechanics) + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + $('.nav-link[href="#form-claim"]').addClass('active'); + $('#form-claim').addClass('active'); @endif @endif @@ -1905,34 +2145,34 @@ use Illuminate\Support\Facades\Auth; } else if (successMessage.toLowerCase().includes('penerimaan') || (successMessage.toLowerCase().includes('mutasi') && successMessage.toLowerCase().includes('diterima')) || activeTab === 'penerimaan') { // For penerimaan mutasi, just refresh the table without resetting form // Activate the correct tab first - $('.nav-link[href="#stock"]').tab('show'); + $('.nav-link[href="#stock"]').addClass('active'); + $('#stock').addClass('active'); + $('.nav-link[href="#penerimaan"]').addClass('active'); + $('#penerimaan').addClass('active'); + // Stay on penerimaan tab and refresh table setTimeout(function() { - $('.nav-link[href="#penerimaan"]').tab('show'); - // Stay on penerimaan tab and refresh table - setTimeout(function() { - if (receiveMutationsTable && $.fn.DataTable.isDataTable('#receiveMutationsTable')) { - receiveMutationsTable.ajax.reload(null, false); // Don't reset paging - } else { - // Initialize table if not already initialized - initReceiveMutationsTable(); - } - }, 300); + if (receiveMutationsTable && $.fn.DataTable.isDataTable('#receiveMutationsTable')) { + receiveMutationsTable.ajax.reload(null, false); // Don't reset paging + } else { + // Initialize table if not already initialized + initReceiveMutationsTable(); + } }, 100); // Close any open modals $('#mutationDetailModal').modal('hide'); } else if (successMessage.toLowerCase().includes('disetujui') || successMessage.toLowerCase().includes('ditolak') || activeTab === 'persetujuan') { // For approval/rejection, refresh the table - $('.nav-link[href="#stock"]').tab('show'); + $('.nav-link[href="#stock"]').addClass('active'); + $('#stock').addClass('active'); + $('.nav-link[href="#penerimaan"]').addClass('active'); + $('#penerimaan').addClass('active'); + // Refresh table to show updated status setTimeout(function() { - $('.nav-link[href="#penerimaan"]').tab('show'); - // Refresh table to show updated status - setTimeout(function() { - if (receiveMutationsTable && $.fn.DataTable.isDataTable('#receiveMutationsTable')) { - receiveMutationsTable.ajax.reload(null, false); - } else { - initReceiveMutationsTable(); - } - }, 300); + if (receiveMutationsTable && $.fn.DataTable.isDataTable('#receiveMutationsTable')) { + receiveMutationsTable.ajax.reload(null, false); + } else { + initReceiveMutationsTable(); + } }, 100); } else if (successMessage.toLowerCase().includes('opname') || activeTab === 'opname') { // Reset opname form after success @@ -2165,12 +2405,7 @@ use Illuminate\Support\Facades\Auth; } }); - // Initialize select2 when mutasi tab is shown - $('a[href="#mutasi"]').on('shown.bs.tab', function (e) { - setTimeout(function() { - initMutasiSelect2(); - }, 100); - }); + // Validate quantity against available stock for mutasi $(document).on('input', '.quantity-input-mutasi', function() { @@ -2275,6 +2510,43 @@ use Illuminate\Support\Facades\Auth; }); } + // Initialize DataTable for claim transactions + var claimTransactionsTable; + + function initClaimTransactionsTable() { + if (claimTransactionsTable) { + claimTransactionsTable.destroy(); + } + + claimTransactionsTable = $('#claimTransactionsTable').DataTable({ + processing: true, + serverSide: true, + ajax: { + url: '{{ route("transaction.get-claim-transactions") }}', + data: { + dealer_id: {{ $mechanic->dealer_id }} + } + }, + columns: [ + {data: 'date', name: 'date'}, + {data: 'spk', name: 'spk'}, + {data: 'police_number', name: 'police_number'}, + {data: 'work_name', name: 'work_name'}, + {data: 'qty', name: 'qty'}, + {data: 'sa_name', name: 'sa_name'}, + {data: 'status', name: 'status', orderable: false}, + {data: 'action', name: 'action', orderable: false, searchable: false} + ], + pageLength: 15, + responsive: true, + scrollX: true, + order: [[0, 'desc']], // Sort by date descending + language: { + url: "//cdn.datatables.net/plug-ins/1.10.24/i18n/Indonesian.json" + } + }); + } + // Function removed since we use single table for both receive and approval mutations // Show mutation detail modal @@ -2622,46 +2894,263 @@ use Illuminate\Support\Facades\Auth; } }); - // Initialize table when tab is shown - $('a[href="#penerimaan"]').on('shown.bs.tab', function (e) { - setTimeout(function() { - initReceiveMutationsTable(); - }, 100); - }); - + + + // Functions for claim transactions + function viewTransaction(transactionId) { + // Show transaction detail modal + Swal.fire({ + title: 'Detail Transaksi', + html: 'Memuat data...', + showConfirmButton: false, + allowOutsideClick: false + }); + + // Load transaction detail via AJAX + $.ajax({ + url: '{{ route("transaction.edit", ":id") }}'.replace(':id', transactionId), + method: 'GET', + success: function(response) { + if (response.status === 200) { + var transaction = response.data; + Swal.fire({ + title: 'Detail Transaksi', + html: ` + + + No. SPK: + ${transaction.spk} + + + No. Polisi: + ${transaction.police_number} + + + Tanggal: + ${transaction.date} + + + Quantity: + ${transaction.qty} + + + Warranty: + ${transaction.warranty == 1 ? 'Ya' : 'Tidak'} + + + `, + confirmButtonText: 'Tutup' + }); + } else { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Gagal memuat detail transaksi' + }); + } + }, + error: function() { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Terjadi kesalahan saat memuat data' + }); + } + }); + } + + function editTransaction(transactionId) { + // Redirect to edit page or show edit modal + window.location.href = '{{ route("transaction.edit", ":id") }}'.replace(':id', transactionId); + } + + function deleteTransaction(transactionId) { + Swal.fire({ + title: 'Konfirmasi Hapus', + text: 'Apakah Anda yakin ingin menghapus transaksi ini?', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Ya, Hapus!', + cancelButtonText: 'Batal' + }).then((result) => { + if (result.isConfirmed) { + // Send delete request + $.ajax({ + url: '{{ route("transaction.destroy", ":id") }}'.replace(':id', transactionId), + method: 'DELETE', + data: { + _token: '{{ csrf_token() }}' + }, + success: function(response) { + Swal.fire({ + icon: 'success', + title: 'Berhasil!', + text: 'Transaksi berhasil dihapus', + timer: 2000, + showConfirmButton: false + }).then(() => { + // Refresh the table + if (claimTransactionsTable) { + claimTransactionsTable.ajax.reload(); + } + }); + }, + error: function() { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Gagal menghapus transaksi' + }); + } + }); + } + }); + } + + function claimTransaction(transactionId) { + Swal.fire({ + title: 'Konfirmasi Klaim', + text: 'Apakah Anda yakin ingin mengklaim pekerjaan ini?', + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#28a745', + cancelButtonColor: '#6c757d', + confirmButtonText: 'Ya, Klaim!', + cancelButtonText: 'Batal' + }).then((result) => { + if (result.isConfirmed) { + // Send claim request + $.ajax({ + url: '{{ route("transaction.claim", ":id") }}'.replace(':id', transactionId), + method: 'POST', + data: { + _token: '{{ csrf_token() }}' + }, + success: function(response) { + Swal.fire({ + icon: 'success', + title: 'Berhasil!', + text: 'Pekerjaan berhasil diklaim', + timer: 2000, + showConfirmButton: false + }).then(() => { + // Refresh the table + if (claimTransactionsTable) { + claimTransactionsTable.ajax.reload(); + } + // Refresh KPI progress bar + refreshKpiProgress(); + }); + }, + error: function() { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Gagal mengklaim pekerjaan' + }); + } + }); + } + }); + } + + // Function to refresh KPI progress bar + function refreshKpiProgress() { + $.ajax({ + url: '{{ route("transaction.get-kpi-data") }}', + method: 'GET', + success: function(response) { + if (response.success) { + var kpiData = response.data; + + // Update target value using ID + $('#kpi-target').text(kpiData.target + ' Pekerjaan'); + + // Update actual value using ID and update class for color + $('#kpi-actual').text(kpiData.actual + ' Pekerjaan').removeClass().addClass('text-' + kpiData.status_color); + + // Update percentage using ID and update class for color + $('#kpi-percentage').text(kpiData.percentage + '%').removeClass().addClass('text-' + kpiData.status_color + ' font-weight-bold'); + + // Update progress bar + var progressWidth = Math.min(100, kpiData.percentage); + $('.progress-bar').css('width', progressWidth + '%'); + $('.progress-bar').attr('aria-valuenow', kpiData.percentage); + $('.progress-bar').removeClass().addClass('progress-bar bg-' + kpiData.status_color); + + // Update status message + var statusMessage = ''; + if (kpiData.status === 'exceeded') { + statusMessage = ' Target tercapai!'; + } else if (kpiData.status === 'good') { + statusMessage = ' Performa baik'; + } else if (kpiData.status === 'fair') { + statusMessage = ' Perlu peningkatan'; + } else if (kpiData.status === 'poor') { + statusMessage = ' Perlu perbaikan'; + } else { + statusMessage = ' Belum ada data'; + } + $('.mt-1').html(statusMessage); + + console.log('KPI Data updated:', kpiData); + } + }, + error: function(xhr, status, error) { + console.log('Failed to refresh KPI data:', error); + console.log('Response:', xhr.responseText); + } + }); + } + // Note: Approve and reject buttons removed from transaction page // These actions are now only available to admin users in the admin panel - // Save active tab to localStorage - $('.nav-link').on('click', function() { + // Handle main tab switching properly (only for main tabs) + $('.nav-tabs-line-primary .nav-link').on('click', function(e) { + e.preventDefault(); var target = $(this).attr('href'); if (target) { - localStorage.setItem('activeTab', target); - } - }); - - // Restore active tab from localStorage if no server-side active tab - $(document).ready(function() { - var serverActiveTab = '{{ session("active_tab") }}'; - - // Only restore from localStorage if no server-side active tab - if (!serverActiveTab) { - var savedTab = localStorage.getItem('activeTab'); - if (savedTab) { - // Activate main tab first if it's a sub-tab - if (savedTab.includes('opname') || savedTab.includes('mutasi') || savedTab.includes('penerimaan')) { - $('.nav-link[href="#stock"]').tab('show'); + // Handle main tab switching + if (target === '#transaksi' || target === '#stock') { + // Remove active from all main tabs and tab panes + $('.nav-tabs-line-primary .nav-link').removeClass('active'); + $('#transaksi, #stock').removeClass('active'); + + // Add active to clicked main tab + $(this).addClass('active'); + $(target).addClass('active'); + + // Handle sub-tab activation based on main tab + if (target === '#transaksi') { + // Remove active from all stock sub-tabs + $('.nav-link[href="#opname"], .nav-link[href="#mutasi"], .nav-link[href="#penerimaan"]').removeClass('active'); + $('#opname, #mutasi, #penerimaan').removeClass('active'); + + // Always activate form-kerja by default when switching to transaksi tab + $('.nav-link[href="#form-kerja"]').addClass('active'); + $('#form-kerja').addClass('active'); + } else if (target === '#stock') { + // Remove active from all transaksi sub-tabs + $('.nav-link[href="#form-kerja"], .nav-link[href="#form-cuci"], .nav-link[href="#form-claim"]').removeClass('active'); + $('#form-kerja, #form-cuci, #form-claim').removeClass('active'); + + // Always activate opname by default when switching to stock tab + $('.nav-link[href="#opname"]').addClass('active'); + $('#opname').addClass('active'); setTimeout(function() { - $('.nav-link[href="' + savedTab + '"]').tab('show'); - }, 100); - } else { - $('.nav-link[href="' + savedTab + '"]').tab('show'); + updateProductCounter(); + }, 50); } } } }); + + function createTransaction(form) { let work_ids; @@ -2710,6 +3199,163 @@ use Illuminate\Support\Facades\Auth; } }) } + + + + + + + // Ensure transaksi tab is shown by default + $(document).ready(function() { + // First, ensure all tabs are properly hidden + $('#transaksi, #stock').removeClass('active'); + $('.nav-tabs-line-primary .nav-link').removeClass('active'); + + // Check if we have specific form values from old input + @if(old('form')) + var oldForm = '{{ old("form") }}'; + + if (oldForm === 'work' || oldForm === 'wash' || oldForm === 'claim') { + // Activate transaksi tab and specific sub-tab + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + + // Remove any active stock sub-tabs + $('.nav-link[href="#opname"], .nav-link[href="#mutasi"], .nav-link[href="#penerimaan"]').removeClass('active'); + $('#opname, #mutasi, #penerimaan').removeClass('active'); + + if (oldForm === 'work') { + $('.nav-link[href="#form-kerja"]').addClass('active'); + $('#form-kerja').addClass('active'); + } else if (oldForm === 'wash') { + $('.nav-link[href="#form-cuci"]').addClass('active'); + $('#form-cuci').addClass('active'); + } else if (oldForm === 'claim') { + $('.nav-link[href="#form-claim"]').addClass('active'); + $('#form-claim').addClass('active'); + } + } else if (oldForm === 'opname' || oldForm === 'mutasi' || oldForm === 'penerimaan') { + // Activate stock tab and specific sub-tab + $('.nav-link[href="#stock"]').addClass('active'); + $('#stock').addClass('active'); + + // Remove any active transaksi sub-tabs + $('.nav-link[href="#form-kerja"], .nav-link[href="#form-cuci"], .nav-link[href="#form-claim"]').removeClass('active'); + $('#form-kerja, #form-cuci, #form-claim').removeClass('active'); + + if (oldForm === 'opname') { + $('.nav-link[href="#opname"]').addClass('active'); + $('#opname').addClass('active'); + updateProductCounter(); + } else if (oldForm === 'mutasi') { + $('.nav-link[href="#mutasi"]').addClass('active'); + $('#mutasi').addClass('active'); + } else if (oldForm === 'penerimaan') { + $('.nav-link[href="#penerimaan"]').addClass('active'); + $('#penerimaan').addClass('active'); + } + } + @else + // If no tab is active, activate transaksi tab by default + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + + // Activate form-kerja by default in transaksi tab + $('.nav-link[href="#form-kerja"]').addClass('active'); + $('#form-kerja').addClass('active'); + @endif + + // Ensure at least one main tab is active + if (!$('.nav-tabs-line-primary .nav-link.active').length) { + $('.nav-link[href="#transaksi"]').addClass('active'); + $('#transaksi').addClass('active'); + + // Ensure at least one sub-tab is active in transaksi + if (!$('#transaksi .tab-content .tab-pane.active').length) { + $('.nav-link[href="#form-kerja"]').addClass('active'); + $('#form-kerja').addClass('active'); + } + } + + // Initialize components based on active tabs + if ($('#opname').hasClass('active') || $('#stock').hasClass('active')) { + updateProductCounter(); + } + + if ($('#mutasi').hasClass('active')) { + setTimeout(function() { + initMutasiSelect2(); + updateRemoveButtonsMutasi(); + }, 100); + } + + if ($('#penerimaan').hasClass('active')) { + setTimeout(function() { + initReceiveMutationsTable(); + }, 100); + } + + if ($('#form-claim').hasClass('active')) { + setTimeout(function() { + initClaimTransactionsTable(); + }, 100); + } + }); + + + + + + + + // Handle sub-tab switching for transaksi tabs + $('#transaksi .nav-tabs-line-success .nav-link').on('click', function(e) { + e.preventDefault(); + var target = $(this).attr('href'); + + // Remove active from all transaksi sub-tabs + $('#transaksi .nav-tabs-line-success .nav-link').removeClass('active'); + $('#transaksi .tab-content .tab-pane').removeClass('active'); + + // Add active to clicked sub-tab + $(this).addClass('active'); + $(target).addClass('active'); + + // Initialize components based on which sub-tab is shown + if (target === '#form-claim') { + setTimeout(function() { + initClaimTransactionsTable(); + }, 100); + } + }); + + // Handle sub-tab switching for stock tabs + $('#stock .nav-tabs-line-success .nav-link').on('click', function(e) { + e.preventDefault(); + var target = $(this).attr('href'); + + // Remove active from all stock sub-tabs + $('#stock .nav-tabs-line-success .nav-link').removeClass('active'); + $('#stock .tab-content .tab-pane').removeClass('active'); + + // Add active to clicked sub-tab + $(this).addClass('active'); + $(target).addClass('active'); + + // Initialize components based on which sub-tab is shown + if (target === '#opname') { + updateProductCounter(); + } else if (target === '#mutasi') { + setTimeout(function() { + initMutasiSelect2(); + updateRemoveButtonsMutasi(); + }, 100); + } else if (target === '#penerimaan') { + setTimeout(function() { + initReceiveMutationsTable(); + }, 100); + } + }); @endsection \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 644e390..a7af76b 100755 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ use App\Http\Controllers\WarehouseManagement\OpnamesController; use App\Http\Controllers\WarehouseManagement\ProductCategoriesController; use App\Http\Controllers\WarehouseManagement\ProductsController; use App\Http\Controllers\WorkController; +use App\Http\Controllers\WorkDealerPriceController; use App\Http\Controllers\WarehouseManagement\MutationsController; use App\Http\Controllers\WarehouseManagement\StockAuditController; use App\Http\Controllers\KPI\TargetsController; @@ -166,8 +167,15 @@ Route::group(['middleware' => 'auth'], function() { // Stock Management Routes Route::post('/transaction/check-stock', [TransactionController::class, 'checkStockAvailability'])->name('transaction.check-stock'); Route::get('/transaction/stock-prediction', [TransactionController::class, 'getStockPrediction'])->name('transaction.stock-prediction'); + + // Claim Transactions Route + Route::get('/transaction/get-claim-transactions', [TransactionController::class, 'getClaimTransactions'])->name('transaction.get-claim-transactions'); + Route::post('/transaction/claim/{id}', [TransactionController::class, 'claim'])->name('transaction.claim'); }); + // KPI Data Route - accessible to all authenticated users + Route::get('/transaction/get-kpi-data', [TransactionController::class, 'getKpiData'])->name('transaction.get-kpi-data'); + Route::group(['prefix' => 'admin', 'middleware' => 'adminRole'], function() { Route::get('/dashboard2', [AdminController::class, 'dashboard2'])->name('dashboard2'); Route::post('/dealer_work_trx', [AdminController::class, 'dealer_work_trx'])->name('dealer_work_trx'); @@ -182,6 +190,23 @@ Route::group(['middleware' => 'auth'], function() { Route::resource('category', CategoryController::class); Route::resource('work', WorkController::class); + // Work Dealer Prices Routes + Route::prefix('work/{work}/prices')->name('work.prices.')->controller(WorkDealerPriceController::class)->group(function () { + Route::get('/', 'index')->name('index'); + Route::post('/', 'store')->name('store'); + Route::get('{price}/edit', 'edit')->name('edit'); + Route::put('{price}', 'update')->name('update'); + Route::delete('{price}', 'destroy')->name('destroy'); + Route::post('bulk', 'bulkCreate')->name('bulk-create'); + Route::post('toggle-status', 'toggleStatus')->name('toggle-status'); + }); + + // Route untuk halaman set harga (menggunakan WorkController) + Route::get('work/{work}/set-prices', [WorkController::class, 'showPrices'])->name('work.set-prices'); + + // API route untuk mendapatkan harga + Route::get('work/get-price', [WorkDealerPriceController::class, 'getPrice'])->name('work.get-price'); + // Work Products Management Routes Route::prefix('work/{work}/products')->name('work.products.')->controller(App\Http\Controllers\WorkProductController::class)->group(function () { Route::get('/', 'index')->name('index');
Memuat data...