'form_up_country', 'vehicle' => 'form_vehicle_running_cost', 'entertainment' => 'form_entertainment_presentation', 'meeting' => 'form_meeting_seminar', 'others' => 'form_others' ]; public function index(Request $request) { $user = auth()->user(); $roleName = trim($user->getRoleNames()->first() ?? ''); $now = Carbon::now(); // 1. FILTER WAKTU (Default ke bulan berjalan) $chartMonth = $request->chart_month ?? $now->format('m'); $chartYear = $request->chart_year ?? $now->format('Y'); $monthName = Carbon::createFromFormat('m', $chartMonth)->translatedFormat('F'); // --- KALKULASI PERIODE PENILAIAN (11 di bulan terpilih s/d 13 di bulan berikutnya) --- $displayStart = Carbon::create((int)$chartYear, (int)$chartMonth, 11)->startOfDay(); $displayEnd = $displayStart->copy()->addMonth()->day(13)->endOfDay(); // Dikunci aman ke tanggal 13 penutupan closing laporan $periodeLabelFormatted = $displayStart->translatedFormat('d F') . ' - ' . $displayEnd->translatedFormat('d F Y'); // 2. FILTER OTORISASI & DATA $allowedCabangIds = $this->getScopedCabangIds($user, $roleName); if (empty($allowedCabangIds)) { return view('backend.pages.dashboard.index', $this->emptyData()); } $availableRegions = $this->getAvailableRegions($user, $roleName, $allowedCabangIds); $cabangOptions = Cabang::whereIn('id', $allowedCabangIds) ->when($request->region_id, fn($q) => $q->where('region_id', $request->region_id)) ->get(); // 3. AMBIL DATA PENDING & PERFORMA (Passing Variable Waktu Siklus ke Chart Method) $cabangDetails = $this->getPendingExpenseTable($allowedCabangIds, $request); $performance = $this->getPerformanceCharts($allowedCabangIds, $displayStart, $displayEnd); $totalKategori = Kategori::whereNotNull('account_number')->where('account_number', '!=', '')->count(); $data = [ 'periodeLabel' => $periodeLabelFormatted, 'totalRegions' => Region::count(), 'totalCabangs' => count($allowedCabangIds), 'totalKategori' => $totalKategori, 'regions' => $availableRegions, 'cabangOptions' => $cabangOptions, 'cabangDetails' => $cabangDetails, 'pendingCount' => $this->getTotalPendingGlobal($allowedCabangIds), ]; return view('backend.pages.dashboard.index', array_merge($data, $performance)); } // ======================================================================== // PRIVATE METHODS - SEPARATION OF CONCERNS // ======================================================================== /** * MENGAMBIL DATA UNTUK TABEL MONITORING PENDING */ private function getPendingExpenseTable($allowedCabangIds, Request $request) { $query = Cabang::whereIn('id', $allowedCabangIds)->with('region'); if ($request->region_id) $query->where('region_id', $request->region_id); if ($request->cabang_id) $query->where('id', $request->cabang_id); if ($request->filled('search')) $query->where('name', 'like', "%{$request->search}%"); $sortField = $request->get('sort', 'name'); $sortOrder = $request->get('direction', 'asc'); if ($sortField == 'region') { $query->leftJoin('region', 'cabang.region_id', '=', 'region.id') ->select('cabang.*')->orderBy('region.name', $sortOrder); } else { $query->orderBy('cabang.' . ($sortField == 'total_pending' ? 'name' : $sortField), $sortOrder); } $cabangDetails = $query->paginate(100)->withQueryString()->through(function($cabang) { $statuses = ['Pending', 'On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval']; $details = []; $total = 0; foreach ($this->tables as $key => $table) { $count = DB::table($table)->where('cabang_id', $cabang->id)->whereIn('status', $statuses)->count(); $details[$key] = $count; $total += $count; } $cabang->pending_details = $details; $cabang->total_pending = $total; return $cabang; }); if ($sortField == 'total_pending') { $sortedCollection = $cabangDetails->getCollection()->sortBy('total_pending', SORT_REGULAR, $sortOrder == 'desc')->values(); $cabangDetails->setCollection($sortedCollection); } return $cabangDetails; } /** * MENGHITUNG PERFORMA KECEPATAN APPROVE CABANG (Fokus Akurat Jendela Siklus Transaksi Tanggal 11 - 13) */ private function getPerformanceCharts($cabangIds, Carbon $startDate, Carbon $endDate): array { $queryUnion = null; foreach ($this->tables as $table) { $q = DB::table($table) ->select('cabang_id', 'created_at', 'final_approved_at', 'updated_at') ->where('status', 'Closed') ->whereIn('cabang_id', $cabangIds) // KUNCI UTAMA: Hanya membaca pengajuan yang dibuat murni di dalam rentang tanggal siklus 11 s/d 13 berjalan ->whereBetween('tanggal', [$startDate->format('Y-m-d H:i:s'), $endDate->format('Y-m-d H:i:s')]); $queryUnion = is_null($queryUnion) ? $q : $queryUnion->unionAll($q); } if ($queryUnion && DB::table(DB::raw("({$queryUnion->toSql()}) as t"))->mergeBindings($queryUnion)->exists()) { // Kalkulasi selisih waktu rata-rata penyelesaian berkas menggunakan fondasi TIMESTAMPDIFF detik secara objektif $res = DB::table(DB::raw("({$queryUnion->toSql()}) as combined")) ->mergeBindings($queryUnion) ->select('cabang_id', DB::raw('AVG(TIMESTAMPDIFF(SECOND, created_at, COALESCE(final_approved_at, updated_at))) as avg_sec')) ->groupBy('cabang_id') ->get(); $cabangNames = Cabang::whereIn('id', $res->pluck('cabang_id'))->pluck('name', 'id'); $fastest = $res->sortBy('avg_sec')->take(5); $slowest = $res->sortByDesc('avg_sec')->take(5); return [ 'chartFastest' => [ 'labels' => $fastest->map(fn($i) => $cabangNames[$i->cabang_id] ?? 'N/A')->values(), 'data' => $fastest->map(fn($i) => round($i->avg_sec / 3600, 2))->values(), 'raw' => $fastest->map(fn($i) => $this->formatDuration((int)$i->avg_sec))->values() ], 'chartSlowest' => [ 'labels' => $slowest->map(fn($i) => $cabangNames[$i->cabang_id] ?? 'N/A')->values(), 'data' => $slowest->map(fn($i) => round($i->avg_sec / 3600, 2))->values(), 'raw' => $slowest->map(fn($i) => $this->formatDuration((int)$i->avg_sec))->values() ] ]; } return [ 'chartFastest' => ['labels'=>[], 'data'=>[], 'raw'=>[]], 'chartSlowest' => ['labels'=>[], 'data'=>[], 'raw'=>[]] ]; } /** * GET AVAILABLE REGIONS BERDASARKAN ROLE */ private function getAvailableRegions($user, $roleName, $allowedCabangIds) { $regionsQuery = Region::query(); if (in_array($roleName, ['Admin Region', 'Marketing Operational Manager Region'])) { $regionsQuery->where('id', $user->region_id); } elseif (in_array($roleName, ['Area Manager Cabang', 'Medical Representatif'])) { $myRegionIds = Cabang::whereIn('id', $allowedCabangIds)->pluck('region_id')->unique(); $regionsQuery->whereIn('id', $myRegionIds); } return $regionsQuery->get(); } private function getScopedCabangIds($user, $roleName): array { $central = ['Superadmin', 'Head of Sales Marketing', 'Marketing Information System', 'Accounting']; if (in_array($roleName, $central)) return Cabang::pluck('id')->toArray(); if (in_array($roleName, ['Admin Region', 'Marketing Operational Manager Region'])) { return $user->region_id ? Cabang::where('region_id', $user->region_id)->pluck('id')->toArray() : []; } if (in_array($roleName, ['Area Manager Cabang', 'Medical Representatif'])) { $cabangId = $user->cabang->cabang_id ?? \App\Models\Rayon::where('id', $user->rayon_id)->value('cabang_id'); return $cabangId ? [(int)$cabangId] : []; } return []; } private function getTotalPendingGlobal($cabangIds): int { $total = 0; foreach ($this->tables as $table) { $total += DB::table($table)->whereIn('cabang_id', $cabangIds)->whereIn('status', ['Pending', 'On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval'])->count(); } return $total; } private function formatDuration(int $sec): string { if ($sec <= 0) return "0 Jam"; $d = floor($sec / 86400); $h = floor(($sec % 86400) / 3600); return $d > 0 ? "{$d} Hari {$h} Jam" : "{$h} Jam"; } private function emptyData(): array { return [ 'periodeLabel' => '-', 'totalRegions' => 0, 'totalCabangs' => 0, 'totalKategori' => 0, 'pendingCount' => 0, 'cabangDetails' => collect(), 'chartFastest' => ['labels'=>[], 'data'=>[], 'raw'=>[]], 'chartSlowest' => ['labels'=>[], 'data'=>[], 'raw'=>[]], 'regions' => collect(), 'cabangOptions' => collect() ]; } public function notifications() { return response()->json(['status' => 'success', 'data' => []]); } public function show($id) { try { $user = auth()->user(); $allowedIds = $this->getScopedCabangIds($user, $user->getRoleNames()->first()); if (!in_array((int)$id, $allowedIds)) abort(403); $cabang = Cabang::with('region')->findOrFail($id); $statuses = ['Pending', 'On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval']; $data = []; foreach (['Up Country' => 'form_up_country', 'Vehicle' => 'form_vehicle_running_cost', 'Entertainment' => 'form_entertainment_presentation', 'Meeting' => 'form_meeting_seminar', 'Others' => 'form_others'] as $label => $table) { $data[$label] = DB::table($table)->where('cabang_id', $id)->whereIn('status', $statuses)->get(); } return view('backend.pages.dashboard.show', compact('cabang', 'data')); } catch (\Exception $e) { return redirect()->route('admin.dashboard')->with('error', 'Cabang tidak ditemukan.'); } } }