update
parent
5aa9413e63
commit
9f596bf9e4
|
|
@ -1,214 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use App\Models\Calculation;
|
|
||||||
use App\Enums\SigdStatus;
|
|
||||||
use App\Models\ActivityLock;
|
|
||||||
use App\Models\ReferenceWs;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
|
|
||||||
class EmisiCalculationQueue extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'calculation:process';
|
|
||||||
protected $description = 'Process for the worksheet emission calculation queue';
|
|
||||||
private $logFilePath;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
set_time_limit(0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$processing = Calculation::where('calculation_status', SigdStatus::PROSES)->rowActive()->first();
|
|
||||||
if ($processing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$calculation = Calculation::where('calculation_status', SigdStatus::PENDING)
|
|
||||||
->rowActive()->orderBy('created_at', 'asc')->first();
|
|
||||||
if (!$calculation) {
|
|
||||||
// $this->logMessage('info', 'Semua kalkulasi emisi telah berhasil diselesaikan.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$calculation->update([
|
|
||||||
'executed_time' => now(),
|
|
||||||
'calculation_status' => SigdStatus::PROSES,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->createLogFile($calculation);
|
|
||||||
$this->logMessage('info', 'Kalkulasi Emisi untuk Tahun Inventory: ' . $calculation->inventory_year . ', id: ' . $calculation->id);
|
|
||||||
|
|
||||||
$isLocked = ActivityLock::isLocked($calculation->inventory_year);
|
|
||||||
if ($isLocked) {
|
|
||||||
$this->logMessage('warning', 'Kalkulasi Emisi untuk Tahun Inventory: ' . $calculation->inventory_year . ', id: ' . $calculation->id . ' sedang dikunci dan tidak dapat melakukan kalkulasi.');
|
|
||||||
|
|
||||||
$calculation->update([
|
|
||||||
'finished_time' => now(),
|
|
||||||
'calculation_status' => SigdStatus::GAGAL,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($calculation->energy) $this->executeService('energy', $calculation);
|
|
||||||
if ($calculation->agriculture) $this->executeService('agriculture', $calculation);
|
|
||||||
if ($calculation->folu) $this->executeService('folu', $calculation);
|
|
||||||
if ($calculation->waste) $this->executeService('waste', $calculation);
|
|
||||||
if ($calculation->ippu) $this->executeService('ippu', $calculation);
|
|
||||||
|
|
||||||
// Execute Energy GPC emission calculation
|
|
||||||
$this->energyGPCCalc($calculation);
|
|
||||||
|
|
||||||
// Execute mapping IPCC to GPC
|
|
||||||
$this->gpcMapping($calculation);
|
|
||||||
|
|
||||||
$calculation->update([
|
|
||||||
'finished_time' => now(),
|
|
||||||
'calculation_status' => SigdStatus::SELESAI,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->logMessage('info', 'Kalkulasi Emisi untuk Tahun Inventory: ' . $calculation->inventory_year . ', id: ' . $calculation->id . ' berhasil diselesaikan.');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->logMessage('error', 'Terjadi kesalahan saat kalkulasi emisi, id: ' . $calculation->id . '. Error: ' . $e->getMessage());
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private function createLogFile(Calculation $calculation)
|
|
||||||
{
|
|
||||||
$logDirectory = storage_path('logs/calculation/');
|
|
||||||
|
|
||||||
$this->logMessage('info', 'Checking if log directory exists: ' . $logDirectory);
|
|
||||||
|
|
||||||
if (!File::exists($logDirectory)) {
|
|
||||||
$this->logMessage('info', 'Directory not found. Creating directory: ' . $logDirectory);
|
|
||||||
File::makeDirectory($logDirectory, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$logFileName = $logDirectory . $calculation->inventory_year . '_' . $calculation->id . '.log';
|
|
||||||
file_put_contents($logFileName, '');
|
|
||||||
|
|
||||||
$this->logFilePath = $logFileName;
|
|
||||||
$this->logMessage('info', 'Log file created: ' . $logFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function executeService($sector, $calculation)
|
|
||||||
{
|
|
||||||
// if ($calculation->$sector) {
|
|
||||||
$wsList = ReferenceWs::where('sector', $sector)
|
|
||||||
->whereNotNull('code')->rowActive()->get();
|
|
||||||
|
|
||||||
foreach ($wsList as $ws) {
|
|
||||||
if ($ws->code == '4d1_ef') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = "\\App\\Services\\Emisi\\" . class_basename($ws->model) . "Service";
|
|
||||||
$this->logMessage('service yang dijalankan', "{$service}");
|
|
||||||
|
|
||||||
try {
|
|
||||||
$serviceClass = app($service);
|
|
||||||
if (method_exists($service, 'save')) {
|
|
||||||
$serviceClass->save($ws->code, $calculation->inventory_year);
|
|
||||||
$this->logMessage('info', "Kalkulasi Emisi pada Worksheet {$ws->ws_code}. {$ws->ws_title} telah berhasil diselesaikan.");
|
|
||||||
} else {
|
|
||||||
$this->logMessage('error', "{$service} tidak memiliki method proses kalkulasi.");
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->logMessage('error', "Error saat menjalankan {$service} pada sektor {$sector}: " . $e->getMessage());
|
|
||||||
|
|
||||||
$calculation->update([
|
|
||||||
'executed_time' => null,
|
|
||||||
'finished_time' => null,
|
|
||||||
'calculation_status' => SigdStatus::PENDING,
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private function gpcMapping($calculation)
|
|
||||||
{
|
|
||||||
$service = "\\App\\Services\\Emisi\\GpcMappingService";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$serviceClass = app($service);
|
|
||||||
if (method_exists($service, 'save')) {
|
|
||||||
$serviceClass->save($calculation->inventory_year);
|
|
||||||
$this->logMessage('info', "Mapping dari Emisi IPCC ke GPC telah berhasil diproses");
|
|
||||||
} else {
|
|
||||||
$this->logMessage('error', get_class($serviceClass) . " tidak memiliki method save.");
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->logMessage('error', "Error saat menjalankan " . get_class($serviceClass) . ": " . $e->getMessage());
|
|
||||||
|
|
||||||
$calculation->update([
|
|
||||||
'executed_time' => null,
|
|
||||||
'finished_time' => null,
|
|
||||||
'calculation_status' => SigdStatus::PENDING,
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function energyGPCCalc($calculation)
|
|
||||||
{
|
|
||||||
$service = "\\App\\Services\\Emisi\\EnergyGPCService";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$serviceClass = app($service);
|
|
||||||
if (method_exists($service, 'save')) {
|
|
||||||
$serviceClass->save($calculation->inventory_year);
|
|
||||||
$this->logMessage('info', "Kalkulasi Emisi Energi GPC telah berhasil diproses");
|
|
||||||
} else {
|
|
||||||
$this->logMessage('error', get_class($serviceClass) . " tidak memiliki method save.");
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->logMessage('error', "Error saat menjalankan " . get_class($serviceClass) . ": " . $e->getMessage());
|
|
||||||
|
|
||||||
$calculation->update([
|
|
||||||
'executed_time' => null,
|
|
||||||
'finished_time' => null,
|
|
||||||
'calculation_status' => SigdStatus::PENDING,
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private function logMessage($level, $message)
|
|
||||||
{
|
|
||||||
$this->line($message); // Output to console
|
|
||||||
|
|
||||||
// Log to the file
|
|
||||||
$formattedMessage = '[' . now() . '] ' . strtoupper($level) . ': ' . $message . PHP_EOL;
|
|
||||||
if ($this->logFilePath) {
|
|
||||||
file_put_contents($this->logFilePath, $formattedMessage, FILE_APPEND);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info($message);
|
|
||||||
// Also log to Laravel's default log channel
|
|
||||||
if ($level === 'error') {
|
|
||||||
Log::error($message);
|
|
||||||
}
|
|
||||||
// } else {
|
|
||||||
// Log::info($message);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\DashboardAdaptationService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DashboardAdaptasiController extends Controller
|
||||||
|
{
|
||||||
|
protected $title = 'Dashboard Adaptasi';
|
||||||
|
// protected $template = 'modules.dashboard.adaptasi';
|
||||||
|
protected $route = 'modules.dashboard.adaptasi';
|
||||||
|
protected DashboardAdaptationService $adaptationService;
|
||||||
|
|
||||||
|
public function __construct(DashboardAdaptationService $adaptationService)
|
||||||
|
{
|
||||||
|
$this->adaptationService = $adaptationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aksi(Request $request)
|
||||||
|
{
|
||||||
|
$year = (int) $request->input('adaptationYear', date('Y'));
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dashboardData = $this->adaptationService->getDashboardData($year);
|
||||||
|
return view('auth.dashboard-adaptation-aksi', [
|
||||||
|
'title' => $this->title,
|
||||||
|
'route' => $this->route,
|
||||||
|
'stats' => $dashboardData['stats'],
|
||||||
|
'charts' => $dashboardData['charts'],
|
||||||
|
'today' => Carbon::today()->translatedFormat('d F Y'),
|
||||||
|
]);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
Log::error('Error loading aksi mitigasi dashboard', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->withErrors(['error' => 'Gagal memuat dashboard aksi mitigasi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$year = (int) $request->input('adaptationYear', date('Y'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dashboardData = $this->adaptationService->getDashboardData($year);
|
||||||
|
|
||||||
|
return view('auth.dashboard-adaptation', [
|
||||||
|
'title' => $this->title,
|
||||||
|
'route' => $this->route,
|
||||||
|
'selectedYear' => $dashboardData['adaptationYear'],
|
||||||
|
'tableData' => $dashboardData['tableData'], // ⬅️ collection biasa
|
||||||
|
'barData' => $dashboardData['barData'],
|
||||||
|
'realisasiData' => $dashboardData['realisasiData'],
|
||||||
|
'budgetData' => $dashboardData['budgetData'],
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error loading dashboard adaptation data', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// optional: kasih feedback ke user biar nggak blank
|
||||||
|
return back()->withErrors(['error' => 'Gagal memuat dashboard adaptasi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Services\DashboardService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
protected $title = 'Dashboard Inventory';
|
||||||
|
protected $template = 'auth';
|
||||||
|
protected $route = 'modules.dashboard.inventory';
|
||||||
|
protected $emissionService;
|
||||||
|
|
||||||
|
public function __construct(DashboardService $emissionService)
|
||||||
|
{
|
||||||
|
$this->emissionService = $emissionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data['inventoryYear'] = $request->input('year', date('Y'));
|
||||||
|
|
||||||
|
$data['dashboardData'] = $this->emissionService->getDashboardData($data['inventoryYear']);
|
||||||
|
$data['title'] = $this->title;
|
||||||
|
$data['route'] = $this->route;
|
||||||
|
|
||||||
|
return view($this->template.'.dashboard', $data);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error loading dashboard data', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\DashboardMitigationService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DashboardMitigasiController extends Controller
|
||||||
|
{
|
||||||
|
protected $title = 'Dashboard Mitigasi';
|
||||||
|
// protected $template = 'modules.dashboard.mitigasi';
|
||||||
|
protected $route = 'modules.dashboard.mitigasi';
|
||||||
|
|
||||||
|
protected DashboardMitigationService $mitigationService;
|
||||||
|
|
||||||
|
public function __construct(DashboardMitigationService $mitigationService)
|
||||||
|
{
|
||||||
|
$this->mitigationService = $mitigationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function aksi(Request $request)
|
||||||
|
{
|
||||||
|
$year = (int) $request->input('mitigationYear', date('Y'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dashboardData = $this->mitigationService->getDashboardData($year);
|
||||||
|
return view('auth.dashboard-mitigation-aksi', [
|
||||||
|
'title' => $this->title,
|
||||||
|
'route' => $this->route,
|
||||||
|
'stats' => $dashboardData['stats'],
|
||||||
|
'charts' => $dashboardData['charts'],
|
||||||
|
'today' => Carbon::today()->translatedFormat('d F Y'),
|
||||||
|
]);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
Log::error('Error loading aksi mitigasi dashboard', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->withErrors(['error' => 'Gagal memuat dashboard aksi mitigasi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$year = (int) $request->input('mitigationYear', date('Y'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dashboardData = $this->mitigationService->getDashboardData($year);
|
||||||
|
|
||||||
|
return view('auth.dashboard-mitigation', [
|
||||||
|
'title' => $this->title,
|
||||||
|
'route' => $this->route,
|
||||||
|
'selectedYear' => $dashboardData['mitigationYear'],
|
||||||
|
'tableData' => $dashboardData['tableData'],
|
||||||
|
'barData' => $dashboardData['barData'],
|
||||||
|
'budgetData' => $dashboardData['budgetData'],
|
||||||
|
'realisasiData' => $dashboardData['realisasiData'],
|
||||||
|
]);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
Log::error('Error loading dashboard mitigation data', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// optional: kasih feedback ke user biar nggak blank
|
||||||
|
return back()->withErrors(['error' => 'Gagal memuat dashboard mitigasi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Models\ActivityCrf;
|
|
||||||
use App\Models\ReferenceGwp;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class DashboardService
|
|
||||||
{
|
|
||||||
public function getDashboardData($inventoryYear)
|
|
||||||
{
|
|
||||||
$gwpData = $this->getActiveGwpRecords()->keyBy('ghg_code');
|
|
||||||
$years = activityYearRange($inventoryYear, false, 10);
|
|
||||||
|
|
||||||
$crfData = ActivityCrf::with(['ws.form', 'sektor'])
|
|
||||||
->where('inventory_year', $inventoryYear)
|
|
||||||
->whereBetween('activity_year', [$years[0], end($years)])
|
|
||||||
->orderBy('ws_code', 'asc')->get();
|
|
||||||
|
|
||||||
$aggregatedData = $this->aggregateCrfData($crfData, $gwpData);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'cardData' => $this->calculateCardData($aggregatedData, $inventoryYear),
|
|
||||||
'barData' => $this->calculateBarData($aggregatedData, $years),
|
|
||||||
'pieData' => $this->calculatePieData($aggregatedData, $inventoryYear),
|
|
||||||
'tableData' => $this->calculateTableData($aggregatedData, $inventoryYear),
|
|
||||||
'inventoryYear' => $inventoryYear,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function aggregateCrfData(Collection $crfData, Collection $gwpData)
|
|
||||||
{
|
|
||||||
return $crfData->map(function ($data) use ($gwpData) {
|
|
||||||
|
|
||||||
$sector = $data->sector;
|
|
||||||
$name = $sector === 'energy' ? $data->ws->form->name : $data->sektor->name;
|
|
||||||
|
|
||||||
return [
|
|
||||||
'year' => $data->activity_year,
|
|
||||||
'sector' => $sector,
|
|
||||||
'name' => $name,
|
|
||||||
'co2eq' => $data->co2eq,
|
|
||||||
'co2' => $data->co2,
|
|
||||||
'ch4' => $data->ch4,
|
|
||||||
'n2o' => $data->n2o,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calculateCardData(Collection $aggregatedData, $inventoryYear)
|
|
||||||
{
|
|
||||||
$sectorSums = $aggregatedData->where('year', $inventoryYear - 1)->groupBy('sector')->map->sum('co2eq');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'totalEmisi' => $aggregatedData->where('year', $inventoryYear - 1)->sum('co2eq'),
|
|
||||||
'emisiEnergi' => $sectorSums->get('energy', 0),
|
|
||||||
'emisiPertanian' => $sectorSums->get('agriculture', 0),
|
|
||||||
'emisiLahan' => $sectorSums->get('folu', 0),
|
|
||||||
'emisiLimbah' => $sectorSums->get('waste', 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calculateBarData(Collection $aggregatedData, array $years)
|
|
||||||
{
|
|
||||||
return array_map(function ($year) use ($aggregatedData) {
|
|
||||||
$yearlyData = $aggregatedData->where('year', $year)->groupBy('name')->map->sum('co2eq');
|
|
||||||
$total = $yearlyData->sum();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'year' => $year,
|
|
||||||
'total' => $total,
|
|
||||||
'emisi' => $yearlyData,
|
|
||||||
];
|
|
||||||
}, $years);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calculatePieData(Collection $aggregatedData, $inventoryYear)
|
|
||||||
{
|
|
||||||
return $aggregatedData->where('year', $inventoryYear - 1)->groupBy('name')->map->sum('co2eq');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calculateTableData(Collection $aggregatedData, $inventoryYear)
|
|
||||||
{
|
|
||||||
return $aggregatedData->where('year', $inventoryYear - 1)->groupBy('name')->map(function ($groupedData) {
|
|
||||||
return [
|
|
||||||
'co2eq' => $groupedData->sum('co2eq'),
|
|
||||||
'co2' => $groupedData->sum('co2'),
|
|
||||||
'ch4' => $groupedData->sum('ch4'),
|
|
||||||
'n2o' => $groupedData->sum('n2o'),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getActiveGwpRecords()
|
|
||||||
{
|
|
||||||
return ReferenceGwp::whereHas('ar', function ($query) {
|
|
||||||
$query->isActive();
|
|
||||||
})->get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class AdaptationYearSelect extends Component
|
||||||
|
{
|
||||||
|
public $selectedYear;
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
public function __construct($selectedYear = null, $name = null)
|
||||||
|
{
|
||||||
|
$this->selectedYear = $selectedYear;
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
return view('modules.components.adaptation-year-select');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class MitigationYearSelect extends Component
|
||||||
|
{
|
||||||
|
public $selectedYear;
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
public function __construct($selectedYear = null, $name = null)
|
||||||
|
{
|
||||||
|
$this->selectedYear = $selectedYear;
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
return view('modules.components.mitigation-year-select');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('title', 'Aksi Perubahan Iklim')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid my-4" style="font-family: 'Inter', sans-serif;">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="mb-0 font-weight-bold text-teal">AKSI PERUBAHAN IKLIM</h5>
|
||||||
|
<small class="text-muted">
|
||||||
|
Last update: <span class="text-danger">{{ $today }}</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistik Atas -->
|
||||||
|
<div class="row text-center mb-4">
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL AKSI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_aksi'], 0, ',', '.') }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL ALOKASI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_anggaran']/1000000000, 1, ',', '.') }}</h2>
|
||||||
|
<small>MILIAR IDR</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL REALISASI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_alokasi']/1000000000, 1, ',', '.') }}</h2>
|
||||||
|
<small>MILIAR IDR</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$colors = ['#a98ded', '#5ab4e5', '#f6d55c', '#ed6a5a', '#6cc070', '#ff8c42'];
|
||||||
|
$labelsAksi = collect($charts['aksi'])->pluck('sektor')->toArray();
|
||||||
|
$valuesAksi = collect($charts['aksi'])->pluck('persen')->toArray();
|
||||||
|
|
||||||
|
$labelsAnggaran = collect($charts['anggaran'])->pluck('sektor')->toArray();
|
||||||
|
$valuesAnggaran = collect($charts['anggaran'])->pluck('persen')->toArray();
|
||||||
|
|
||||||
|
$labelsAlokasi = collect($charts['alokasi'])->pluck('sektor')->toArray();
|
||||||
|
$valuesAlokasi = collect($charts['alokasi'])->pluck('persen')->toArray();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<!-- Bagian Chart -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Chart Aksi -->
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL AKSI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartAksi"></canvas>
|
||||||
|
<div class="mt-4">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['aksi'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Anggaran -->
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL ALOKASI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartAnggaran"></canvas>
|
||||||
|
<div class="mt-4">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['anggaran'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Alokasi -->
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL REALISASI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartAlokasi"></canvas>
|
||||||
|
<div class="mt-4">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['alokasi'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('js')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
const colors = @json($colors);
|
||||||
|
|
||||||
|
const configPie = (labels, data) => ({
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
data: data,
|
||||||
|
backgroundColor: colors.slice(0, labels.length),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('chartAksi'), configPie(@json($labelsAksi), @json($valuesAksi)));
|
||||||
|
new Chart(document.getElementById('chartAnggaran'), configPie(@json($labelsAnggaran), @json($valuesAnggaran)));
|
||||||
|
new Chart(document.getElementById('chartAlokasi'), configPie(@json($labelsAlokasi), @json($valuesAlokasi)));
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="card shadow-sm" style="font-family: 'Inter', sans-serif;">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0 font-weight-bold">{{ @$title }}</h5>
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
id="yearForm"
|
||||||
|
action="{{ route($route.'.index') }}"
|
||||||
|
class="d-inline"
|
||||||
|
>
|
||||||
|
<x-adaptation-year-select
|
||||||
|
:selectedYear="$selectedYear"
|
||||||
|
name="adaptationYear"
|
||||||
|
onchange="document.getElementById('yearForm').submit()"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Alerts -->
|
||||||
|
|
||||||
|
<div class="row flex justify-content-end items-center ml-6 mb-4">
|
||||||
|
<div class="flex gap-2 mr-4">
|
||||||
|
<a href="{{ route('modules.formAdaptasi.create', ['reset' => 1]) }}">
|
||||||
|
<button class="button-action btn btn-success">Aksi Baru</button>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route($route.'.aksi') }}">
|
||||||
|
<button class="button-action btn btn-primary">
|
||||||
|
Aksi Perubahan Iklim
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{{-- Charts Row --}}
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<p class="h6">AKSI</p>
|
||||||
|
<canvas id="aksiChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<p class="h6">ALOKASI (Miliar Rupiah)</p>
|
||||||
|
<canvas id="anggaranChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<p class="h6">REALISASI (Miliar Rupiah)</p>
|
||||||
|
<canvas id="realisasiChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Table --}}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="adaptasiTable" class="table table-bordered table-striped align-middle">
|
||||||
|
<thead class="table-info text-white text-center">
|
||||||
|
<tr>
|
||||||
|
<th>Nama Kegiatan</th>
|
||||||
|
<th>Tahun</th>
|
||||||
|
<th>Bidang PRK</th>
|
||||||
|
<th>Sub- Bidang PRK</th>
|
||||||
|
<th style="width:80px;">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($tableData as $a)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $a->nama_kegiatan }}</td>
|
||||||
|
<td class="text-center">{{ $a->tahun_kegiatan }}</td>
|
||||||
|
<td>{{ $a->sektor }}</td>
|
||||||
|
<td>{{ $a->sub_sektor }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{{ route($route.'adaptasi.view', $a->id) }}" class="text-secondary me-2">
|
||||||
|
<i class="ti ti-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route($route.'adaptasi.edit', $a->id) }}" class="text-secondary">
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('css')
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"/>
|
||||||
|
<style>
|
||||||
|
#map { min-height: 600px; }
|
||||||
|
.leaflet-container { background: #fff; }
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('js')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// === DataTables init ===
|
||||||
|
$('#adaptasiTable').DataTable({
|
||||||
|
pageLength: 10,
|
||||||
|
ordering: true,
|
||||||
|
searching: true,
|
||||||
|
lengthChange: true,
|
||||||
|
language: {
|
||||||
|
search: "Cari:",
|
||||||
|
lengthMenu: "Tampilkan _MENU_ data per halaman",
|
||||||
|
zeroRecords: "Tidak ada data ditemukan",
|
||||||
|
emptyTable: "Tidak ada data.",
|
||||||
|
info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ data",
|
||||||
|
infoEmpty: "Tidak ada data tersedia",
|
||||||
|
infoFiltered: "(difilter dari _MAX_ total data)"
|
||||||
|
},
|
||||||
|
footerCallback: function (row, data, start, end, display) {
|
||||||
|
$(this.api().column(0).footer()).html('TOTAL (Semua Tahun {{ $selectedYear }})');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Util warna untuk dataset ===
|
||||||
|
function getColor(i) {
|
||||||
|
const palette = [
|
||||||
|
'rgba(255,99,132,0.6)',
|
||||||
|
'rgba(54,162,235,0.6)',
|
||||||
|
'rgba(255,206,86,0.6)',
|
||||||
|
'rgba(75,192,192,0.6)',
|
||||||
|
'rgba(153,102,255,0.6)',
|
||||||
|
'rgba(255,159,64,0.6)',
|
||||||
|
'rgba(199,199,199,0.6)',
|
||||||
|
'rgba(100,181,246,0.6)',
|
||||||
|
'rgba(76,175,80,0.6)',
|
||||||
|
'rgba(121,85,72,0.6)'
|
||||||
|
];
|
||||||
|
return palette[i % palette.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Formatter Miliar Rupiah (2 desimal, trailing zero dipangkas oleh Intl) ===
|
||||||
|
// function fmtMiliarFromBillion(b) {
|
||||||
|
// return new Intl.NumberFormat('id-ID', { maximumFractionDigits: 2 }).format(b) + ' M';
|
||||||
|
// }
|
||||||
|
function parseIdrToNumber(v) {
|
||||||
|
if (typeof v === 'number') return v;
|
||||||
|
if (v == null) return 0;
|
||||||
|
const s = String(v).replace(/\s/g,'').replace(/\./g,'').replace(',', '.');
|
||||||
|
const n = Number(s);
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
function toBillion(v) { return parseIdrToNumber(v) / 1_000_000_000; }
|
||||||
|
function pickDecimals(maxAbs) {
|
||||||
|
if (maxAbs >= 1) return 2;
|
||||||
|
if (maxAbs >= 0.1) return 3;
|
||||||
|
if (maxAbs >= 0.01) return 4;
|
||||||
|
if (maxAbs >= 0.001) return 5;
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
function fmtMiliar(v, d) {
|
||||||
|
return new Intl.NumberFormat('id-ID', { maximumFractionDigits: d }).format(v) + ' M';
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Chart AKSI (bukan rupiah) ===
|
||||||
|
const barData = @json($barData);
|
||||||
|
const years = barData.map(d => d.year);
|
||||||
|
const sectors = barData.length ? Object.keys(barData[0].jumlah || {}) : [];
|
||||||
|
const aksiDatasets = sectors.map((sec, idx) => ({
|
||||||
|
label: sec,
|
||||||
|
data: years.map(y => {
|
||||||
|
const row = barData.find(d => d.year === y) || {};
|
||||||
|
return (row.jumlah && row.jumlah[sec]) ? row.jumlah[sec] : 0;
|
||||||
|
}),
|
||||||
|
backgroundColor: getColor(idx),
|
||||||
|
borderColor: getColor(idx).replace('0.6','1'),
|
||||||
|
borderWidth: 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Chart(document.getElementById('aksiChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: { labels: years, datasets: aksiDatasets },
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y}` } }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Jumlah Kegiatan' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Chart Anggaran & Realisasi (Rupiah → Miliar) ===
|
||||||
|
const budget = @json($budgetData);
|
||||||
|
const realisasi = @json($realisasiData);
|
||||||
|
const labels = ['APBN','APBD','Swasta','Lain-lain'];
|
||||||
|
|
||||||
|
const anggaranValuesB = [
|
||||||
|
toBillion(budget?.apbn), toBillion(budget?.apbd),
|
||||||
|
toBillion(budget?.swasta), toBillion(budget?.lainlain)
|
||||||
|
];
|
||||||
|
const realisasiValuesB = [
|
||||||
|
toBillion(realisasi?.apbn), toBillion(realisasi?.apbd),
|
||||||
|
toBillion(realisasi?.swasta), toBillion(realisasi?.lainlain)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tentukan jumlah desimal per chart berdasarkan nilai maksimum
|
||||||
|
const decAnggaran = pickDecimals(Math.max(...anggaranValuesB.map(Math.abs), 0));
|
||||||
|
const decRealisasi = pickDecimals(Math.max(...realisasiValuesB.map(Math.abs), 0));
|
||||||
|
|
||||||
|
const colors = ['#0575E6','#FFA500','#8A2BE2','#00BFA6'];
|
||||||
|
const realisasiColors = ['#7ed957','#ffd966','#bdb2ff','#80cbc4'];
|
||||||
|
|
||||||
|
function createBarChart(canvasId, datasetLabel, valuesB, colorsArr, decimals) {
|
||||||
|
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||||
|
|
||||||
|
const datasets = labels.map((label, i) => ({
|
||||||
|
label: label,
|
||||||
|
data: [valuesB[i]], // satu nilai per kategori
|
||||||
|
backgroundColor: colorsArr[i],
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'end',
|
||||||
|
align: 'right',
|
||||||
|
formatter: v => fmtMiliar(v, decimals),
|
||||||
|
clip: false
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [datasetLabel], // cuma 1 kategori "Anggaran" atau "Realisasi"
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true, position: 'top' },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => {
|
||||||
|
const v = ctx.parsed.x ?? ctx.parsed.y ?? 0;
|
||||||
|
return `${ctx.dataset.label}: ${fmtMiliar(v, decimals)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
beginAtZero: true,
|
||||||
|
suggestedMax: Math.max(...valuesB) * 1.1 || 1,
|
||||||
|
title: { display: true, text: 'Miliar Rupiah (M)' },
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => fmtMiliar(value, decimals)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: { ticks: { autoSkip: false } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [ ChartDataLabels ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createBarChart('anggaranChart', '', anggaranValuesB, colors, decAnggaran);
|
||||||
|
createBarChart('realisasiChart', '', realisasiValuesB, realisasiColors, decRealisasi);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('title', 'Aksi Perubahan Iklim')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid my-4" style="font-family: 'Inter', sans-serif;">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="mb-0 font-weight-bold text-teal">AKSI PERUBAHAN IKLIM</h5>
|
||||||
|
<small class="text-muted">
|
||||||
|
Last update: <span class="text-danger">{{ $today }}</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistik Atas -->
|
||||||
|
<div class="row text-center mb-4">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL AKSI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_aksi'], 0, ',', '.') }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL PENURUNAN EMISI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_emisi'], 0, ',', '.') }}</h2>
|
||||||
|
<small>TON CO₂</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL ALOKASI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_anggaran']/1000000000, 1, ',', '.') }}</h2>
|
||||||
|
<small>MILIAR IDR</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="p-3 border rounded bg-light">
|
||||||
|
<h6 class="text-muted">TOTAL REALISASI</h6>
|
||||||
|
<h2 class="text-success">{{ number_format($stats['total_alokasi']/1000000000, 1, ',', '.') }}</h2>
|
||||||
|
<small>MILIAR IDR</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$colors = ['#a98ded', '#5ab4e5', '#f6d55c', '#ed6a5a', '#6cc070', '#ff8c42'];
|
||||||
|
$labelsAksi = collect($charts['aksi'])->pluck('sektor')->toArray();
|
||||||
|
$valuesAksi = collect($charts['aksi'])->pluck('persen')->toArray();
|
||||||
|
|
||||||
|
$labelsEmisi = collect($charts['emisi'])->pluck('sektor')->toArray();
|
||||||
|
$valuesEmisi = collect($charts['emisi'])->pluck('persen')->toArray();
|
||||||
|
|
||||||
|
$labelsAnggaran = collect($charts['anggaran'])->pluck('sektor')->toArray();
|
||||||
|
$valuesAnggaran = collect($charts['anggaran'])->pluck('persen')->toArray();
|
||||||
|
|
||||||
|
$labelsAlokasi = collect($charts['alokasi'])->pluck('sektor')->toArray();
|
||||||
|
$valuesAlokasi = collect($charts['alokasi'])->pluck('persen')->toArray();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<!-- Bagian Chart -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Chart Aksi -->
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL AKSI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartAksi"></canvas>
|
||||||
|
<div class="mt-3">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['aksi'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Emisi -->
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL PENURUNAN EMISI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartEmisi"></canvas>
|
||||||
|
<div class="mt-3">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['emisi'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Anggaran -->
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL ALOKASI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartAnggaran"></canvas>
|
||||||
|
<div class="mt-3">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['anggaran'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Alokasi -->
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-success text-white text-center">TOTAL REALISASI</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartAlokasi"></canvas>
|
||||||
|
<div class="mt-3">
|
||||||
|
<small>
|
||||||
|
@foreach($charts['alokasi'] as $i => $row)
|
||||||
|
<span class="d-block">
|
||||||
|
<span style="color: {{ $colors[$i % count($colors)] }};">
|
||||||
|
{{ $row['persen'] }}%
|
||||||
|
</span> {{ $row['sektor'] }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('js')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
const colors = @json($colors);
|
||||||
|
|
||||||
|
const configPie = (labels, data) => ({
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
data: data,
|
||||||
|
backgroundColor: colors.slice(0, labels.length),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('chartAksi'), configPie(@json($labelsAksi), @json($valuesAksi)));
|
||||||
|
new Chart(document.getElementById('chartEmisi'), configPie(@json($labelsEmisi), @json($valuesEmisi)));
|
||||||
|
new Chart(document.getElementById('chartAnggaran'), configPie(@json($labelsAnggaran), @json($valuesAnggaran)));
|
||||||
|
new Chart(document.getElementById('chartAlokasi'), configPie(@json($labelsAlokasi), @json($valuesAlokasi)));
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="card shadow-sm" style="font-family: 'Inter', sans-serif;">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Dasbor Mitigasi</h5>
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
id="yearForm"
|
||||||
|
action="{{ route('modules.dashboard.mitigasi.index') }}"
|
||||||
|
class="d-inline"
|
||||||
|
>
|
||||||
|
<x-mitigation-year-select
|
||||||
|
:selectedYear="$selectedYear"
|
||||||
|
name="mitigationYear"
|
||||||
|
onchange="document.getElementById('yearForm').submit()"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="row flex justify-content-end items-center ml-6 mb-4">
|
||||||
|
<div class="flex gap-2 mr-4">
|
||||||
|
<a href="{{ route('modules.formMitigasi.create', ['reset' => 1]) }}">
|
||||||
|
<button class="button-action btn btn-success">
|
||||||
|
Aksi Baru
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route($route.'.aksi') }}">
|
||||||
|
<button class="button-action btn btn-primary text-white hover:bg-green-700">
|
||||||
|
Aksi Perubahan Iklim
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{{-- Charts Row --}}
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<p class="h6">AKSI</p>
|
||||||
|
<canvas id="aksiChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<p class="h6">ALOKASI (Miliar Rupiah)</p>
|
||||||
|
<canvas id="anggaranChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<p class="h6">REALISASI (Miliar Rupiah)</p>
|
||||||
|
<canvas id="realisasiChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Table --}}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="mitigasiTable" class="table table-bordered table-striped align-middle">
|
||||||
|
<thead class="table-info text-white text-center">
|
||||||
|
<tr>
|
||||||
|
<th>Tipe Kegiatan</th>
|
||||||
|
<th>Nama Kegiatan</th>
|
||||||
|
<th>Tahun</th>
|
||||||
|
<th>Sektor</th>
|
||||||
|
<th>Sub-Sektor</th>
|
||||||
|
<th>Kategori</th>
|
||||||
|
<th class="text-end">Penurunan Emisi</th>
|
||||||
|
<th class="text-center">Revisi</th>
|
||||||
|
<th style="width:80px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($tableData as $m)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $m->tipe_kegiatan }}</td>
|
||||||
|
<td>{{ Str::limit($m->nama_kegiatan, 60) }}</td>
|
||||||
|
<td class="text-center">{{ $m->tahun_kegiatan }}</td>
|
||||||
|
<td>{{ $m->sektor }}</td>
|
||||||
|
<td>{{ $m->sub_sektor }}</td>
|
||||||
|
<td>{{ $m->kategori_perhitungan }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
{{ $m->emission_factor }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{{ $m->revisi ?? '--' }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{{ route($route.'.view', $m->id) }}" class="text-secondary me-2">
|
||||||
|
<i class="ti ti-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route($route.'.edit', $m->id) }}" class="text-secondary">
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('css')
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"/>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('js')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// === DataTables (unchanged) ===
|
||||||
|
$('#mitigasiTable').DataTable({
|
||||||
|
pageLength: 10, ordering: true, searching: true, lengthChange: true,
|
||||||
|
columnDefs: [
|
||||||
|
{ targets: 6, className: 'text-end' },
|
||||||
|
{ targets: 7, className: 'text-center' },
|
||||||
|
{ targets: 8, className: 'text-center', orderable: false, searchable: false }
|
||||||
|
],
|
||||||
|
language: {
|
||||||
|
search: "Cari:", lengthMenu: "Tampilkan _MENU_ data per halaman",
|
||||||
|
zeroRecords: "Tidak ada data ditemukan", emptyTable: "Tidak ada data.",
|
||||||
|
info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ data",
|
||||||
|
infoEmpty: "Tidak ada data tersedia",
|
||||||
|
infoFiltered: "(difilter dari _MAX_ total data)"
|
||||||
|
},
|
||||||
|
footerCallback: function () {
|
||||||
|
const api = this.api();
|
||||||
|
$(api.column(0).footer()).html('TOTAL (Semua Tahun {{ $selectedYear }})');
|
||||||
|
const parseIdNumber = (v) => {
|
||||||
|
if (typeof v === 'number') return v;
|
||||||
|
if (!v) return 0;
|
||||||
|
const s = String(v).replace(/\./g, '').replace(',', '.');
|
||||||
|
const n = parseFloat(s);
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
};
|
||||||
|
const total = api.column(6, { search: 'applied' }).data().toArray()
|
||||||
|
.reduce((sum, val) => sum + parseIdNumber(val), 0);
|
||||||
|
const formatted = new Intl.NumberFormat('id-ID', { maximumFractionDigits: 6 }).format(total);
|
||||||
|
$(api.column(6).footer()).html(formatted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Colors ===
|
||||||
|
function getColor(i) {
|
||||||
|
const p = [
|
||||||
|
'rgba(255,99,132,0.6)','rgba(54,162,235,0.6)','rgba(255,206,86,0.6)',
|
||||||
|
'rgba(75,192,192,0.6)','rgba(153,102,255,0.6)','rgba(255,159,64,0.6)',
|
||||||
|
'rgba(199,199,199,0.6)','rgba(100,181,246,0.6)','rgba(76,175,80,0.6)',
|
||||||
|
'rgba(121,85,72,0.6)'
|
||||||
|
];
|
||||||
|
return p[i % p.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Parser & Formatter (IDR → Miliar, desimal adaptif) ===
|
||||||
|
function parseIdrToNumber(v) {
|
||||||
|
if (typeof v === 'number') return v;
|
||||||
|
if (v == null) return 0;
|
||||||
|
const s = String(v).replace(/\s/g,'').replace(/\./g,'').replace(',', '.');
|
||||||
|
const n = Number(s);
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
function toBillion(v) { return parseIdrToNumber(v) / 1_000_000_000; }
|
||||||
|
function pickDecimals(maxAbs) {
|
||||||
|
if (maxAbs >= 1) return 2;
|
||||||
|
if (maxAbs >= 0.1) return 3;
|
||||||
|
if (maxAbs >= 0.01) return 4;
|
||||||
|
if (maxAbs >= 0.001) return 5;
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
function fmtMiliar(v, d) {
|
||||||
|
return new Intl.NumberFormat('id-ID', { maximumFractionDigits: d }).format(v) + ' M';
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Chart AKSI (bukan rupiah) ===
|
||||||
|
const barData = @json($barData);
|
||||||
|
const years = barData.map(d => d.year);
|
||||||
|
const sectors = barData.length ? Object.keys(barData[0].jumlah || {}) : [];
|
||||||
|
const aksiDatasets = sectors.map((sec, idx) => ({
|
||||||
|
label: sec,
|
||||||
|
data: years.map(y => {
|
||||||
|
const row = barData.find(d => d.year === y) || {};
|
||||||
|
return (row.jumlah && row.jumlah[sec]) ? row.jumlah[sec] : 0;
|
||||||
|
}),
|
||||||
|
backgroundColor: getColor(idx),
|
||||||
|
borderColor: getColor(idx).replace('0.6','1'),
|
||||||
|
borderWidth: 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Chart(document.getElementById('aksiChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: { labels: years, datasets: aksiDatasets },
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y}` } }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Jumlah Kegiatan' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Chart Anggaran & Realisasi (Rupiah → Miliar) ===
|
||||||
|
const budget = @json($budgetData);
|
||||||
|
const realisasi = @json($realisasiData);
|
||||||
|
const labels = ['APBN','APBD','Swasta','Lain-lain'];
|
||||||
|
|
||||||
|
const anggaranValuesB = [
|
||||||
|
toBillion(budget?.apbn), toBillion(budget?.apbd),
|
||||||
|
toBillion(budget?.swasta), toBillion(budget?.lainlain)
|
||||||
|
];
|
||||||
|
const realisasiValuesB = [
|
||||||
|
toBillion(realisasi?.apbn), toBillion(realisasi?.apbd),
|
||||||
|
toBillion(realisasi?.swasta), toBillion(realisasi?.lainlain)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tentukan jumlah desimal per chart berdasarkan nilai maksimum
|
||||||
|
const decAnggaran = pickDecimals(Math.max(...anggaranValuesB.map(Math.abs), 0));
|
||||||
|
const decRealisasi = pickDecimals(Math.max(...realisasiValuesB.map(Math.abs), 0));
|
||||||
|
|
||||||
|
const colors = ['#0575E6','#FFA500','#8A2BE2','#00BFA6'];
|
||||||
|
const realisasiColors = ['#7ed957','#ffd966','#bdb2ff','#80cbc4'];
|
||||||
|
|
||||||
|
function createBarChart(canvasId, datasetLabel, valuesB, colorsArr, decimals) {
|
||||||
|
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||||
|
|
||||||
|
const datasets = labels.map((label, i) => ({
|
||||||
|
label: label,
|
||||||
|
data: [valuesB[i]], // satu nilai per kategori
|
||||||
|
backgroundColor: colorsArr[i],
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'end',
|
||||||
|
align: 'right',
|
||||||
|
formatter: v => fmtMiliar(v, decimals),
|
||||||
|
clip: false
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [datasetLabel], // cuma 1 kategori "Anggaran" atau "Realisasi"
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true, position: 'top' },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => {
|
||||||
|
const v = ctx.parsed.x ?? ctx.parsed.y ?? 0;
|
||||||
|
return `${ctx.dataset.label}: ${fmtMiliar(v, decimals)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
beginAtZero: true,
|
||||||
|
suggestedMax: Math.max(...valuesB) * 1.1 || 1,
|
||||||
|
title: { display: true, text: 'Miliar Rupiah (M)' },
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => fmtMiliar(value, decimals)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: { ticks: { autoSkip: false } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [ ChartDataLabels ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createBarChart('anggaranChart', '', anggaranValuesB, colors, decAnggaran);
|
||||||
|
createBarChart('realisasiChart', '', realisasiValuesB, realisasiColors, decRealisasi);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
<?php
|
||||||
|
$cardData = $dashboardData['cardData'];
|
||||||
|
$tableData = $dashboardData['tableData'];
|
||||||
|
$pieData = $dashboardData['pieData'];
|
||||||
|
$barData = $dashboardData['barData'];
|
||||||
|
?>
|
||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0 font-weight-bold">{{ @$title }}</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Filter Tahun Inventory -->
|
||||||
|
<div class="row justify-content-end mb-4">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<x-inventory-year-select :selected-year="$inventoryYear" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards -->
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-2 col-md-4 mb-4">
|
||||||
|
<div class="card h-100 text-center border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title text-muted">Total Emisi</h6>
|
||||||
|
<p class="card-text" style="font-size: 20px;" id="totalEmisi">
|
||||||
|
{{ getFormattedValue(@$cardData['totalEmisi'], 2) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4 mb-4">
|
||||||
|
<div class="card h-100 text-center border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title text-muted">Emisi Sektor Energi</h6>
|
||||||
|
<p class="card-text" style="font-size: 20px;" id="emisiEnergi">
|
||||||
|
{{ getFormattedValue(@$cardData['emisiEnergi'], 2) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4 mb-4">
|
||||||
|
<div class="card h-100 text-center border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title text-muted">Emisi Sektor Pertanian</h6>
|
||||||
|
<p class="card-text" style="font-size: 20px;" id="emisiPertanian">
|
||||||
|
{{ getFormattedValue(@$cardData['emisiPertanian'], 2) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4 mb-4">
|
||||||
|
<div class="card h-100 text-center border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title text-muted">Emisi Sektor Lahan</h6>
|
||||||
|
<p class="card-text" style="font-size: 20px;" id="emisiLahan">
|
||||||
|
{{ getFormattedValue(@$cardData['emisiLahan'], 2) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4 mb-4">
|
||||||
|
<div class="card h-100 text-center border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title text-muted">Emisi Sektor Limbah</h6>
|
||||||
|
<p class="card-text" style="font-size: 20px;" id="emisiLimbah">
|
||||||
|
{{ getFormattedValue(@$cardData['emisiLimbah'], 2) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<!-- Charts Row -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<!-- Bar Chart -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<canvas id="barEmissionChart" height="160"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pie Chart -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<canvas id="pieEmissionChart" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Emissions Table -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<thead class="table-info text-center">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 10px;"></th>
|
||||||
|
<th class="align-middle">Emissions</th>
|
||||||
|
<th width="120">CO<sub>2</sub> Eq<br>(Gg)</th>
|
||||||
|
<th width="120">CO<sub>2</sub><br>(Gg)</th>
|
||||||
|
<th width="120">CH<sub>4</sub><br>(Gg)</th>
|
||||||
|
<th width="120">N<sub>2</sub>O<br>(Gg)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@php
|
||||||
|
$totalCo2eq = 0;
|
||||||
|
$totalCo2 = 0;
|
||||||
|
$totalCh4 = 0;
|
||||||
|
$totalN2o = 0;
|
||||||
|
$no = 1;
|
||||||
|
@endphp
|
||||||
|
@if(@$tableData)
|
||||||
|
@foreach ($tableData as $sector => $emissions)
|
||||||
|
@php
|
||||||
|
$totalCo2eq += $emissions['co2eq'];
|
||||||
|
$totalCo2 += $emissions['co2'];
|
||||||
|
$totalCh4 += $emissions['ch4'];
|
||||||
|
$totalN2o += $emissions['n2o'];
|
||||||
|
@endphp
|
||||||
|
<tr class="text-right">
|
||||||
|
<td class="text-left">{{ $no++ }}</td>
|
||||||
|
<td class="text-left">{{ $sector }}</td>
|
||||||
|
<td>{{ getFormattedValue($emissions['co2eq'], 2) }}</td>
|
||||||
|
<td>{{ getFormattedValue($emissions['co2'], 2) }}</td>
|
||||||
|
<td>{{ getFormattedValue($emissions['ch4'], 2) }}</td>
|
||||||
|
<td>{{ getFormattedValue($emissions['n2o'], 2) }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
<tr class="text-right">
|
||||||
|
<td class="text-left"></td>
|
||||||
|
<td class="text-left"><strong>TOTAL</strong></td>
|
||||||
|
<td><strong>{{ getFormattedValue($totalCo2eq, 2) }}</strong></td>
|
||||||
|
<td><strong>{{ getFormattedValue($totalCo2, 2) }}</strong></td>
|
||||||
|
<td><strong>{{ getFormattedValue($totalCh4, 2) }}</strong></td>
|
||||||
|
<td><strong>{{ getFormattedValue($totalN2o, 2) }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('js')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#inventoryYear').select2({
|
||||||
|
placeholder: 'Pilih Tahun',
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#inventoryYear').on('change', function() {
|
||||||
|
var selectedYear = $(this).val();
|
||||||
|
window.location.href = '{{ url()->current() }}?year=' + selectedYear;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define color palette
|
||||||
|
function getColor(index) {
|
||||||
|
const colors = [
|
||||||
|
'rgba(255, 99, 132, 0.6)', // Red
|
||||||
|
'rgba(54, 162, 235, 0.6)', // Blue
|
||||||
|
'rgba(255, 206, 86, 0.6)', // Yellow
|
||||||
|
'rgba(75, 192, 192, 0.6)', // Green
|
||||||
|
'rgba(153, 102, 255, 0.6)', // Purple
|
||||||
|
'rgba(205, 159, 64, 0.6)', // Orange
|
||||||
|
'rgba(199, 199, 199, 0.6)', // Gray
|
||||||
|
'rgba(0, 255, 0, 0.6)', // Light Green
|
||||||
|
'rgba(83, 102, 120, 0.6)', // Dark Gray
|
||||||
|
|
||||||
|
];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data for the bar chart
|
||||||
|
var emissionsByYear = @json(@$barData);
|
||||||
|
|
||||||
|
var labels = emissionsByYear.map(function(e) {
|
||||||
|
return e.year;
|
||||||
|
});
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
labels: labels,
|
||||||
|
datasets: []
|
||||||
|
};
|
||||||
|
|
||||||
|
var yearTotalMap = {};
|
||||||
|
emissionsByYear.forEach(function(yearData) {
|
||||||
|
yearTotalMap[yearData.year] = yearData.total;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collecting sector keys (categories) for chart labels
|
||||||
|
var sectorKeys = Object.keys(emissionsByYear[0].emisi);
|
||||||
|
|
||||||
|
sectorKeys.forEach(function(key, index) {
|
||||||
|
var sectorData = emissionsByYear.map(function(e) {
|
||||||
|
return e.emisi[key] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
data.datasets.push({
|
||||||
|
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||||
|
data: sectorData,
|
||||||
|
backgroundColor: getColor(index),
|
||||||
|
borderColor: getColor(index).replace('0.6', '1'),
|
||||||
|
borderWidth: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var ctx = document.getElementById('barEmissionChart').getContext('2d');
|
||||||
|
var barChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: function(tooltipItems) {
|
||||||
|
// Use the first tooltip item to get the year
|
||||||
|
return 'Year: ' + tooltipItems[0].label;
|
||||||
|
},
|
||||||
|
label: function(context) {
|
||||||
|
var label = context.dataset.label || '';
|
||||||
|
var value = context.raw.toFixed(2);
|
||||||
|
var year = labels[context.dataIndex];
|
||||||
|
var total = yearTotalMap[year].toFixed(2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
label + ': ' + value + ' Gg CO2',
|
||||||
|
'Total: ' + total + ' Gg CO2'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
align: 'start',
|
||||||
|
usePointStyle: true,
|
||||||
|
labels: {
|
||||||
|
boxWidth: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 40 // Adjust this value to create space above the chart to avoid overlap with y-axis labels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// maintainAspectRatio: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var pieEmissions = @json(@$pieData);
|
||||||
|
|
||||||
|
var pieData = {
|
||||||
|
labels: Object.keys(pieEmissions),
|
||||||
|
datasets: [{
|
||||||
|
data: Object.values(pieEmissions),
|
||||||
|
backgroundColor: Object.keys(pieEmissions).map((_, index) => getColor(index)),
|
||||||
|
borderColor: Object.keys(pieEmissions).map((_, index) => getColor(index).replace(
|
||||||
|
'0.6', '1')),
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
var pieCtx = document.getElementById('pieEmissionChart').getContext('2d');
|
||||||
|
var pieChart = new Chart(pieCtx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: pieData,
|
||||||
|
// plugins: [ChartDataLabels],
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
// datalabels: {
|
||||||
|
// formatter: (value, context) => {
|
||||||
|
// var total = context.chart.data.datasets[0].data.reduce((acc, cur) =>
|
||||||
|
// acc + cur, 0);
|
||||||
|
// var percentage = ((value / total) * 100).toFixed(2) + '%';
|
||||||
|
// return percentage;
|
||||||
|
// },
|
||||||
|
// color: '#000', // Set the text color
|
||||||
|
// font: {
|
||||||
|
// weight: 'bold',
|
||||||
|
// size: 10
|
||||||
|
// },
|
||||||
|
// anchor: 'center', // Position the label in the center of the slice
|
||||||
|
// align: 'center' // Align the label to the middle of the slice
|
||||||
|
// },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
var label = context.label || '';
|
||||||
|
var value = context.raw.toFixed(2);
|
||||||
|
var total = Object.values(pieEmissions).reduce((acc, cur) => acc +
|
||||||
|
cur, 0).toFixed(2);
|
||||||
|
var percentage = ((context.raw / total) * 100).toFixed(2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
label + ': ' + value + ' Gg CO2 (' + percentage + '%)',
|
||||||
|
'Total: ' + total + ' Gg CO2'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'right',
|
||||||
|
labels: {
|
||||||
|
boxWidth: 20,
|
||||||
|
usePointStyle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
right: 40 // Adjust this value to create space above the chart to avoid overlap with y-axis labels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cutout: '50%',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\HomeController;
|
use App\Http\Controllers\HomeController;
|
||||||
use App\Http\Controllers\Dashboard\DashboardAdaptasiController;
|
use App\Http\Controllers\DashboardAdaptasiController;
|
||||||
use App\Http\Controllers\Dashboard\DashboardInventoryController;
|
use App\Http\Controllers\DashboardController;
|
||||||
use App\Http\Controllers\Dashboard\DashboardMitigasiController;
|
use App\Http\Controllers\DashboardMitigasiController;
|
||||||
use App\Http\Controllers\Management\UserController;
|
use App\Http\Controllers\Management\UserController;
|
||||||
use App\Http\Controllers\Management\UserSekolahController;
|
use App\Http\Controllers\Management\UserSekolahController;
|
||||||
use App\Http\Controllers\Management\RoleController;
|
use App\Http\Controllers\Management\RoleController;
|
||||||
|
|
@ -59,7 +59,7 @@ Route::get('dashboard',[HomeController::class,'index'])->name('index');
|
||||||
|
|
||||||
Route::name('dashboard.')->prefix('dashboard')->group(function () {
|
Route::name('dashboard.')->prefix('dashboard')->group(function () {
|
||||||
Route::name('inventory.')->prefix('inventory')->group(function () {
|
Route::name('inventory.')->prefix('inventory')->group(function () {
|
||||||
Route::resource('/',DashboardInventoryController::class);
|
Route::resource('/',DashboardController::class);
|
||||||
});
|
});
|
||||||
Route::name('mitigasi.')->prefix('mitigasi')->group(function () {
|
Route::name('mitigasi.')->prefix('mitigasi')->group(function () {
|
||||||
Route::resource('/',DashboardMitigasiController::class);
|
Route::resource('/',DashboardMitigasiController::class);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue