247 lines
11 KiB
PHP
247 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Backend;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Region;
|
|
use App\Models\Cabang;
|
|
use App\Models\Kategori;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Carbon\Carbon;
|
|
|
|
class DashboardController extends Controller
|
|
{
|
|
private array $tables = [
|
|
'up_country' => '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.');
|
|
}
|
|
}
|
|
} |