This commit is contained in:
@@ -5,120 +5,243 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Backend;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Admin;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use App\Models\FormUpCountry;
|
||||
use App\Models\FormEntertaimentPresentation;
|
||||
use App\Models\FormVehicleRunningCost;
|
||||
use App\Models\FormMeetingSeminar;
|
||||
use App\Models\FormOthers;
|
||||
use App\Models\UserHasCabang;
|
||||
use App\Models\Notifications;
|
||||
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
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$role = auth()->user()->getRoleNames()[0];
|
||||
$startDay = (int) env('STARTING_DATE', 11);
|
||||
$closingDay = (int) env('CLOSING_DATE', 10);
|
||||
private array $tables = [
|
||||
'up_country' => 'form_up_country',
|
||||
'vehicle' => 'form_vehicle_running_cost',
|
||||
'entertainment' => 'form_entertainment_presentation',
|
||||
'meeting' => 'form_meeting_seminar',
|
||||
'others' => 'form_others'
|
||||
];
|
||||
|
||||
// Referensi waktu sekarang
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$roleName = trim($user->getRoleNames()->first() ?? '');
|
||||
$now = Carbon::now();
|
||||
|
||||
// Tentukan periode
|
||||
if ($now->day >= $startDay) {
|
||||
// Mulai dari tanggal START bulan ini
|
||||
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
|
||||
// Sampai tanggal CLOSING bulan depan
|
||||
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay)->endOfDay();
|
||||
} else {
|
||||
// Mulai dari tanggal START bulan lalu
|
||||
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
|
||||
// Sampai tanggal CLOSING bulan ini
|
||||
$closingDate = Carbon::create($now->year, $now->month, $closingDay)->endOfDay();
|
||||
// 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());
|
||||
}
|
||||
|
||||
$formQueries = [
|
||||
'vehicle' => FormVehicleRunningCost::query()
|
||||
->whereMonth('tanggal', date('m'))
|
||||
->whereBetween('tanggal', [$startDate, $closingDate]),
|
||||
'upcountry' => FormUpCountry::query()
|
||||
->whereMonth('tanggal', date('m'))
|
||||
->whereBetween('tanggal', [$startDate, $closingDate]),
|
||||
'entertainment' => FormEntertaimentPresentation::query()
|
||||
->whereMonth('tanggal', date('m'))
|
||||
->whereBetween('tanggal', [$startDate, $closingDate]),
|
||||
'meeting' => FormMeetingSeminar::query()
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereBetween('created_at', [$startDate, $closingDate]),
|
||||
'others' => FormOthers::query()
|
||||
->whereMonth('tanggal', date('m'))
|
||||
->whereBetween('tanggal', [$startDate, $closingDate]),
|
||||
$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),
|
||||
];
|
||||
|
||||
$approvedStatuses = [
|
||||
// 'Approved',
|
||||
// 'Approved 1',
|
||||
// 'Approved 2',
|
||||
// 'Final Approved',
|
||||
// 'Final Approve',
|
||||
'Closed',
|
||||
];
|
||||
return view('backend.pages.dashboard.index', array_merge($data, $performance));
|
||||
}
|
||||
|
||||
$forms = [];
|
||||
// ========================================================================
|
||||
// PRIVATE METHODS - SEPARATION OF CONCERNS
|
||||
// ========================================================================
|
||||
|
||||
foreach ($formQueries as $key => $query) {
|
||||
$forms[$key] = $query
|
||||
->whereIn('status', $approvedStatuses)
|
||||
->whereNotNull('final_approved_at')
|
||||
->sum('approved_total');
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
return view(
|
||||
'backend.pages.dashboard.index',
|
||||
[
|
||||
'forms' => $forms,
|
||||
]
|
||||
);
|
||||
$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;
|
||||
}
|
||||
|
||||
public function notifications()
|
||||
/**
|
||||
* MENGHITUNG PERFORMA KECEPATAN APPROVE CABANG (Fokus Akurat Jendela Siklus Transaksi Tanggal 11 - 13)
|
||||
*/
|
||||
private function getPerformanceCharts($cabangIds, Carbon $startDate, Carbon $endDate): array
|
||||
{
|
||||
$this->checkAuthorization(auth()->user(), ['notification.view']);
|
||||
$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);
|
||||
}
|
||||
|
||||
// Mulai dengan query dasar
|
||||
$notifications = Notifications::orderBy('created_at', 'desc')
|
||||
->where('receiver_id', auth()->user()->id)
|
||||
->where('is_read', 0);
|
||||
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();
|
||||
|
||||
// Ambil data dan kembalikan sebagai JSON
|
||||
return response()->json([
|
||||
'count' => $notifications->count(),
|
||||
'data' => $notifications->take(7)->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'=>[]]
|
||||
];
|
||||
}
|
||||
|
||||
public function mark_read($id)
|
||||
/**
|
||||
* GET AVAILABLE REGIONS BERDASARKAN ROLE
|
||||
*/
|
||||
private function getAvailableRegions($user, $roleName, $allowedCabangIds)
|
||||
{
|
||||
$this->checkAuthorization(auth()->user(), ['notification.delete']);
|
||||
$receiver_id = auth()->user()->id;
|
||||
|
||||
Notifications::where('receiver_id', $receiver_id)->where('id', $id)->update(['is_read' => 1]);
|
||||
|
||||
return response()->json(['message' => 'Notification marked as read.']);
|
||||
$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();
|
||||
}
|
||||
|
||||
public function mark_all_read()
|
||||
private function getScopedCabangIds($user, $roleName): array
|
||||
{
|
||||
$this->checkAuthorization(auth()->user(), ['notification.delete']);
|
||||
$receiver_id = auth()->user()->id;
|
||||
$central = ['Superadmin', 'Head of Sales Marketing', 'Marketing Information System', 'Accounting'];
|
||||
if (in_array($roleName, $central)) return Cabang::pluck('id')->toArray();
|
||||
|
||||
Notifications::where('receiver_id', $receiver_id)->update(['is_read' => 1]);
|
||||
if (in_array($roleName, ['Admin Region', 'Marketing Operational Manager Region'])) {
|
||||
return $user->region_id ? Cabang::where('region_id', $user->region_id)->pluck('id')->toArray() : [];
|
||||
}
|
||||
|
||||
session()->flash('success', 'All notifications marked as read.');
|
||||
return redirect()->back();
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user