Files
expense/app/Http/Controllers/Backend/DashboardController.php
T
root 1c1418f3e0
Deploy to VPS (PAT over HTTPS) / deploy (push) Has been cancelled
Initial commit - expense
2026-06-01 15:01:03 +07:00

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.');
}
}
}