dashboard produksi & ganti warna chart

This commit is contained in:
2026-10-13 15:55:18 +07:00
parent aef27704f2
commit 80f0056efe
19 changed files with 994 additions and 334 deletions
@@ -8,7 +8,7 @@ enum FrekuensiPemangkasanPendamping: string
case TIDAK_RUTIN = 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya';
case SATU_KALI_TAHUN = 'Memangkas 1 kali/tahun, tahu jenis tekniknya';
case DUA_KALI_TAHUN = 'Memangkas 2 kali/tahun, tahu jenis tekniknya';
case LEBIH_DUA_KALI_TAHUN = 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin';
case LEBIH_DUA_KALI_TAHUN = 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin';
public function label(): string
{
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanPendamping: string
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
self::DUA_KALI_TAHUN => 'Memangkas 2 kali/tahun, tahu jenis tekniknya',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin',
};
}
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanUtama: int
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
self::DUA_KALI_TAHUN => 'Memangkas 2 kali/tahun, tahu jenis tekniknya',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin',
};
}
@@ -16,8 +16,8 @@ enum MetodePenangananHamaPenyakit: int
self::TIDAK_ADA => 'Tidak ada penanganan',
self::CARA_TRADISIONAL => 'Cara tradisional (doa/ritual)',
self::PESTISIDA_SANITASI => 'Pestisida atau sanitasi',
self::KOMBINASI => 'Kombinasi pestisada dan sanitasi',
self::PHT => 'Pengendalian hama terpadu(PHT)',
self::KOMBINASI => 'Kombinasi pestisida dan sanitasi',
self::PHT => 'Pengendalian hama terpadu (PHT)',
};
}
@@ -5,8 +5,13 @@ namespace App\Http\Controllers\Web\Dashboard;
use App\Enums\KeparahanSerangan;
use App\Enums\KondisiTanaman;
use App\Enums\PenyakitTanaman;
use App\Enums\PraktikBudidaya\FrekuensiPemangkasanUtama;
use App\Enums\PraktikBudidaya\FrekuensiSanitasiLahan;
use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit;
use App\Enums\PraktikBudidaya\PemberianPupuk;
use App\Enums\SeranganHama;
use App\Http\Controllers\Controller;
use App\Models\Pertanian\Komoditas;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
@@ -67,6 +72,7 @@ class SummaryController extends Controller
->get([
'id',
'lahan_id',
'komoditas_id',
'kondisi_tanaman',
'umur_tanaman',
'serangan_hama',
@@ -87,49 +93,189 @@ class SummaryController extends Controller
$landMetrics = $this->karakteristikLandMetrics($request);
$totalLuasLahan = $landMetrics['total_luas_lahan'];
$petaniCount = $landMetrics['total_petani'];
$avgLuasPerPetani = $landMetrics['ratarata_luas_lahan'];
$jumlahTanaman = $conditionSource->whereNotNull('kondisi_tanaman')->count();
$productivityCards = $this->productivityKpiCards($lahanIds);
$praktikBudidaya = $this->filteredPraktikBudidaya($request);
$kpiCards = [
[
'value' => number_format($totalLuasLahan, 2, ',', '.'),
'unit' => 'Ha',
'unit' => 'ha',
'label' => 'Total Luas Lahan',
'description' => 'Luas keseluruhan lahan terfilter',
'description' => 'Seluruh lahan petani',
'icon' => 'tabler-chart-area-line',
'variant' => 'success',
],
[
'value' => number_format($avgLuasPerPetani, 2, ',', '.'),
'unit' => 'Ha/petani',
'label' => 'Rata-Rata Luas Lahan per Petani',
'description' => number_format($petaniCount, 0, ',', '.').' petani terfilter',
'unit' => 'ha',
'label' => 'Rata-rata Luas Lahan',
'description' => 'Rata-rata per petani',
'icon' => 'tabler-ruler-measure',
'variant' => 'danger',
],
[
'value' => number_format($jumlahTanaman, 0, ',', '.'),
'unit' => 'tanaman',
'label' => 'Jumlah Tanaman',
'description' => 'Total tanaman pada chart kondisi tanaman',
'icon' => 'tabler-plant-2',
'variant' => 'secondary',
],
];
$kpiCards = array_merge($kpiCards, $productivityCards);
return [
'kpiCards' => $kpiCards,
'chartData' => [
'umurLahan' => $this->ageBucketChart($lahan, 'umur_lahan'),
'umurTanaman' => $this->ageBucketChart($healthSource, 'umur_tanaman'),
'jumlahTanamanPerKomoditas' => $this->plantCountByCommodityChart($tanaman, $lahan),
'kondisiTanaman' => $this->attributeChart($conditionSource, 'kondisi_tanaman', fn ($value) => $this->conditionLabel($value)),
'keparahanSerangan' => $this->attributeChart($healthSource, 'keparahan_serangan', fn ($value) => $this->severityLabel($value)),
'gejalaSerangan' => $this->symptomChart($healthSource),
'intensitasPemangkasan' => $this->enumOptionChart($praktikBudidaya, 'frekuensi_pemangkasan_utama', FrekuensiPemangkasanUtama::class),
'intensitasPemupukan' => $this->enumOptionChart($praktikBudidaya, 'pemberian_pupuk', PemberianPupuk::class),
'metodePenangananHamaPenyakit' => $this->enumOptionChart($praktikBudidaya, 'metode_penanganan_opt', MetodePenangananHamaPenyakit::class),
'intensitasPembersihanLahan' => $this->enumOptionChart($praktikBudidaya, 'frekuensi_sanitasi_lahan', FrekuensiSanitasiLahan::class),
'sumberAirKebun' => $this->arrayOptionChart(
$praktikBudidaya,
'sumber_air',
['Sungai/mata air', 'Sumur', 'Mengandalkan hujan saja', 'Lainnya'],
fn ($value) => in_array($value, ['Sungai/mata air', 'Sumur', 'Mengandalkan hujan saja'], true) ? $value : 'Lainnya',
),
'kebiasaanPraktikBaik' => $this->arrayOptionChart(
$praktikBudidaya,
'kebiasaan_konservasi_tanah',
[
'Membuat pematang/tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah',
'Menumpuk daun kering/sisa tanaman di sekitar pohon',
'Menanam tanaman lain di sela-sela komoditas utama untuk menutupi tanah',
'Membuat lubang atau parit kecil untuk menampung air hujan',
'Tidak ada yang seperti itu di kebun saya',
],
fn ($value) => $value === 'Membuat pematang atau tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah'
? 'Membuat pematang/tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah'
: $value,
),
],
];
}
private function filteredPraktikBudidaya(Request $request)
{
$petaniIds = $this->filteredPetaniIdsQuery($request);
return DB::table('praktik_budidaya')
->whereNull('praktik_budidaya.deleted_at')
->whereIn('praktik_budidaya.petani_id', $petaniIds)
->get([
'frekuensi_pemangkasan_utama',
'pemberian_pupuk',
'metode_penanganan_opt',
'frekuensi_sanitasi_lahan',
'sumber_air',
'kebiasaan_konservasi_tanah',
]);
}
private function plantCountByCommodityChart($tanaman, $lahan): array
{
$lahanKomoditas = $lahan
->mapWithKeys(fn ($row) => [(int) $row->id => $row->komoditas_nama ?: 'Belum ditentukan']);
$komoditasNames = DB::table('master_komoditas')
->whereNull('deleted_at')
->pluck('nama', 'id');
$counts = $tanaman
->map(function ($row) use ($komoditasNames, $lahanKomoditas) {
if ($row->komoditas_id && $komoditasNames->has($row->komoditas_id)) {
return $komoditasNames[$row->komoditas_id];
}
return $lahanKomoditas[(int) $row->lahan_id] ?? 'Belum ditentukan';
})
->filter()
->countBy()
->sortDesc();
return [
'labels' => $counts->keys()->values()->all(),
'series' => $counts->values()->map(fn ($value) => (int) $value)->values()->all(),
];
}
private function productivityKpiCards($lahanIds): array
{
$komoditas = Komoditas::query()
->where('is_utama', true)
->orderBy('nama')
->limit(4)
->get(['id', 'nama']);
$produksiRows = DB::table('produksi')
->whereNull('produksi.deleted_at')
->leftJoin('map_lahan as lahan_produksi', 'lahan_produksi.id', '=', 'produksi.lahan_id')
->when($lahanIds->isNotEmpty(), fn ($query) => $query->whereIn('produksi.lahan_id', $lahanIds))
->when($lahanIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0'))
->selectRaw('COALESCE(produksi.komoditas_id, lahan_produksi.komoditas_id) as komoditas_id')
->addSelect([
'produksi.jumlah_panen',
'produksi.satuan',
'produksi.luas_panen',
'produksi.produktivitas',
])
->get()
->groupBy('komoditas_id');
$variants = ['primary', 'warning', 'info', 'secondary'];
$icon = 'tabler-chart-bar';
return $komoditas
->values()
->map(function ($item, int $index) use ($produksiRows, $variants, $icon) {
$rows = $produksiRows->get($item->id, collect());
$average = $this->averageProductivity($rows);
return [
'value' => number_format($average, 0, ',', '.'),
'unit' => 'kg/ha',
'label' => 'Produktivitas Rata-rata '.$item->nama,
'description' => 'Rata-rata produktivitas per komoditas',
'icon' => $icon,
'variant' => $variants[$index % count($variants)],
];
})
->all();
}
private function averageProductivity($rows): float
{
if ($rows->isEmpty()) {
return 0;
}
$storedAverage = $rows
->pluck('produktivitas')
->filter(fn ($value) => (float) $value > 0)
->avg();
if ($storedAverage > 0) {
return round((float) $storedAverage, 2);
}
$totalProduksiKg = $rows->sum(fn ($row) => $this->toKilograms(
(float) ($row->jumlah_panen ?? 0),
(string) ($row->satuan ?? 'kg')
));
$totalLuasPanen = $rows->sum(fn ($row) => (float) ($row->luas_panen ?? 0));
return $totalLuasPanen > 0
? round($totalProduksiKg / $totalLuasPanen, 2)
: 0;
}
private function toKilograms(float $value, string $unit): float
{
return match (strtolower($unit)) {
'ton' => $value * 1000,
'kuintal', 'kwintal' => $value * 100,
default => $value,
};
}
private function filteredLahanQuery(Request $request)
{
return DB::table('map_lahan')
@@ -271,6 +417,48 @@ class SummaryController extends Controller
];
}
private function enumOptionChart($rows, string $field, string $enumClass): array
{
$options = collect($enumClass::options())
->mapWithKeys(fn ($option) => [(string) $option['value'] => $option['label']]);
$counts = $rows
->pluck($field)
->filter(fn ($value) => $value !== null && $value !== '')
->map(fn ($value) => (string) $value)
->countBy();
return [
'labels' => $options->values()->all(),
'series' => $options->keys()->map(fn ($value) => (int) ($counts[$value] ?? 0))->values()->all(),
];
}
private function arrayOptionChart($rows, string $field, array $labels, ?callable $normalizer = null): array
{
$counts = collect($labels)->mapWithKeys(fn ($label) => [$label => 0]);
foreach ($rows as $row) {
foreach ($this->arrayValues($row->{$field} ?? null) as $value) {
$label = $normalizer ? $normalizer($value) : $value;
if ($label === null || $label === '') {
continue;
}
if (!$counts->has($label)) {
$counts[$label] = 0;
}
$counts[$label] = (int) $counts[$label] + 1;
}
}
return [
'labels' => $counts->keys()->values()->all(),
'series' => $counts->values()->map(fn ($value) => (int) $value)->values()->all(),
];
}
private function symptomChart($rows): array
{
$counts = collect();
@@ -489,34 +489,34 @@ class SurveyPetaniController extends Controller
{
return [
[
'color' => '#28a745',
'bg' => 'rgba(40,167,69,.15)',
'tx' => '#28a745',
'color' => '#6FD68F',
'bg' => 'rgba(111,214,143,.24)',
'tx' => '#6FD68F',
],
[
'color' => '#007bff',
'bg' => 'rgba(0,123,255,.15)',
'tx' => '#007bff',
'color' => '#5FC7D6',
'bg' => 'rgba(95,199,214,.24)',
'tx' => '#5FC7D6',
],
[
'color' => '#fd7e14',
'bg' => 'rgba(253,126,20,.15)',
'tx' => '#fd7e14',
'color' => '#FFB46F',
'bg' => 'rgba(255,180,111,.26)',
'tx' => '#FFB46F',
],
[
'color' => '#dc3545',
'bg' => 'rgba(220,53,69,.15)',
'tx' => '#dc3545',
'color' => '#F47C86',
'bg' => 'rgba(244,124,134,.26)',
'tx' => '#F47C86',
],
[
'color' => '#6f42c1',
'bg' => 'rgba(111,66,193,.15)',
'tx' => '#6f42c1',
'color' => '#9D8EF0',
'bg' => 'rgba(157,142,240,.26)',
'tx' => '#9D8EF0',
],
[
'color' => '#20c997',
'bg' => 'rgba(32,201,151,.15)',
'tx' => '#20c997',
'color' => '#65D8BD',
'bg' => 'rgba(101,216,189,.26)',
'tx' => '#65D8BD',
],
];
}
+40
View File
@@ -37,6 +37,46 @@ window.config = {
danger: window.Helpers.getCssVar('danger-bg-subtle'),
dark: window.Helpers.getCssVar('dark-bg-subtle')
},
chartColors: {
series: ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'],
semantic: {
primary: '#9D8EF0',
secondary: '#9EA6B0',
success: '#6FD68F',
info: '#5FC7D6',
warning: '#FFB46F',
danger: '#F47C86',
purple: '#9D8EF0',
teal: '#65D8BD',
pink: '#F47C86',
gray: '#9EA6B0'
},
text: {
primary: '#7367F0',
secondary: '#808390',
success: '#28C76F',
info: '#00BAD1',
warning: '#FF9F43',
danger: '#FF4C51',
purple: '#7367F0',
teal: '#20C997',
pink: '#FF4C51',
gray: '#808390'
},
categories: {
coffee: '#6FD68F',
vanilla: '#FFB46F',
cacao: '#9D8EF0',
candlenut: '#9EA6B0',
shade: '#65D8BD',
policyPurple: '#9D8EF0',
policyPink: '#F47C86',
marketGreen: '#6FD68F',
marketRed: '#F47C86',
marketBlue: '#5FC7D6'
},
grid: '#ECF0F6'
},
fontFamily: window.Helpers.getCssVar('font-family-base'),
enableMenuLocalStorage: true // Enable menu state with local storage support
};
+24 -29
View File
@@ -9,6 +9,9 @@
cardColor = config.colors.cardColor;
labelColor = config.colors.textMuted;
headingColor = config.colors.headingColor;
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
// swiper loop and autoplay
// --------------------------------------------------------------------
@@ -48,16 +51,10 @@
grid: {
show: false
},
colors: [config.colors.success],
colors: [pastelSemantic.success || '#6FD68F'],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.4,
gradientToColors: [config.colors.cardColor],
opacityTo: 0.1,
stops: [0, 100]
}
type: 'solid',
opacity: 0.22
},
dataLabels: {
enabled: false
@@ -151,13 +148,13 @@
}
},
colors: [
config.colors_label.primary,
config.colors_label.primary,
config.colors_label.primary,
config.colors_label.primary,
config.colors.primary,
config.colors_label.primary,
config.colors_label.primary
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.primary || '#9D8EF0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0'
],
dataLabels: {
enabled: false
@@ -262,18 +259,10 @@
}
}
},
colors: [config.colors.primary],
colors: [pastelSemantic.primary || '#9D8EF0'],
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
shadeIntensity: 0.5,
gradientToColors: [config.colors.primary],
inverseColors: true,
opacityFrom: 1,
opacityTo: 0.6,
stops: [30, 70, 100]
}
type: 'solid',
opacity: 0.9
},
stroke: {
dashArray: 10
@@ -356,7 +345,10 @@
}
},
colors: [config.colors.primary, config.colors.secondary],
colors: [
pastelSeries[0] || '#9D8EF0',
pastelSeries[1] || '#6FD68F'
],
dataLabels: {
enabled: false
},
@@ -369,7 +361,10 @@
legend: {
show: false
},
colors: [config.colors.primary, config.colors.secondary],
colors: [
pastelSeries[0] || '#9D8EF0',
pastelSeries[1] || '#6FD68F'
],
fill: {
opacity: 1
},
+13 -9
View File
@@ -238,6 +238,10 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -255,7 +259,7 @@
{
label: 'Desa Mengadopsi (Realisasi)',
data: [0, 3, 8, 15], // Kumulatif: 3 Desa Inti + 12 Baru
backgroundColor: '#8b5cf6', // Purple-500
backgroundColor: pastelCategory.policyPurple || '#9D8EF0',
borderRadius: 4,
order: 2
},
@@ -263,12 +267,12 @@
label: 'Target Perluasan ToC',
type: 'line',
data: [3, 5, 10, 15],
borderColor: '#d946ef', // Fuchsia-500
borderColor: pastelCategory.policyPink || '#F47C86',
backgroundColor: 'transparent',
borderWidth: 3,
borderDash: [5, 5],
tension: 0.3,
pointBackgroundColor: '#d946ef',
pointBackgroundColor: pastelCategory.policyPink || '#F47C86',
order: 1
}
]
@@ -280,7 +284,7 @@
scales: {
y: {
beginAtZero: true,
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: { stepSize: 5 }
},
x: { grid: { display: false } }
@@ -309,10 +313,10 @@
datasets: [{
data: [4, 2, 1, 1],
backgroundColor: [
'#f59e0b', // Amber-500
'#10b981', // Emerald-500
'#3b82f6', // Blue-500
'#8b5cf6' // Purple-500
pastelSemantic.warning || '#FFB46F',
pastelSemantic.success || '#6FD68F',
pastelSemantic.info || '#5FC7D6',
pastelCategory.policyPurple || '#9D8EF0'
],
borderWidth: 2,
borderColor: '#ffffff',
@@ -335,4 +339,4 @@
});
</script>
@endpush
@endpush
+27 -27
View File
@@ -7,9 +7,9 @@
transition: transform .2s ease;
}
.tk-stat:hover { transform: translateX(4px); }
.tk-stat.green { border-left-color: #28a745; }
.tk-stat.blue { border-left-color: #007bff; }
.tk-stat.amber { border-left-color: #fd7e14; }
.tk-stat.green { border-left-color: #6FD68F; }
.tk-stat.blue { border-left-color: #5FC7D6; }
.tk-stat.amber { border-left-color: #FFB46F; }
.tk-stat-lbl {
font-size: 10px; text-transform: uppercase;
@@ -88,38 +88,38 @@
'new_month' => 12,
'enumerators' => [
['name'=>'Enumerator Ngada 1','done'=>72,'target'=>80,'color'=>'#28a745','bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
['name'=>'Enumerator Ngada 2','done'=>58,'target'=>80,'color'=>'#007bff','bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
['name'=>'Enumerator Ngada 3','done'=>48,'target'=>80,'color'=>'#fd7e14','bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
['name'=>'Enumerator Ngada 4','done'=>36,'target'=>80,'color'=>'#dc3545','bg'=>'rgba(220,53,69,.15)','tx'=>'#dc3545'],
['name'=>'Enumerator Ngada 1','done'=>72,'target'=>80,'color'=>'#6FD68F','bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
['name'=>'Enumerator Ngada 2','done'=>58,'target'=>80,'color'=>'#5FC7D6','bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
['name'=>'Enumerator Ngada 3','done'=>48,'target'=>80,'color'=>'#FFB46F','bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
['name'=>'Enumerator Ngada 4','done'=>36,'target'=>80,'color'=>'#F47C86','bg'=>'rgba(244,124,134,.26)','tx'=>'#F47C86'],
],
'recent' => [
['code'=>'TKD-2026-0178','name'=>'Markus Bela', 'init'=>'MB','desa'=>'Wangka', 'date'=>'24 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
['code'=>'TKD-2026-0177','name'=>'Theresia Nuka','init'=>'TN','desa'=>'Langa', 'date'=>'23 Jun 2026','pct'=>67, 'status'=>'in_progress','bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
['code'=>'TKD-2026-0176','name'=>'Yosef Meo', 'init'=>'YM','desa'=>'Bobo', 'date'=>'22 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
['code'=>'TKD-2026-0175','name'=>'Maria Seda', 'init'=>'MS','desa'=>'Tana Wolo','date'=>'21 Jun 2026','pct'=>22, 'status'=>'draft', 'bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
['code'=>'TKD-2026-0174','name'=>'Rafael Bolo', 'init'=>'RB','desa'=>'Wangka', 'date'=>'20 Jun 2026','pct'=>11, 'status'=>'draft', 'bg'=>'rgba(220,53,69,.15)','tx'=>'#dc3545'],
['code'=>'TKD-2026-0178','name'=>'Markus Bela', 'init'=>'MB','desa'=>'Wangka', 'date'=>'24 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
['code'=>'TKD-2026-0177','name'=>'Theresia Nuka','init'=>'TN','desa'=>'Langa', 'date'=>'23 Jun 2026','pct'=>67, 'status'=>'in_progress','bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
['code'=>'TKD-2026-0176','name'=>'Yosef Meo', 'init'=>'YM','desa'=>'Bobo', 'date'=>'22 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
['code'=>'TKD-2026-0175','name'=>'Maria Seda', 'init'=>'MS','desa'=>'Tana Wolo','date'=>'21 Jun 2026','pct'=>22, 'status'=>'draft', 'bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
['code'=>'TKD-2026-0174','name'=>'Rafael Bolo', 'init'=>'RB','desa'=>'Wangka', 'date'=>'20 Jun 2026','pct'=>11, 'status'=>'draft', 'bg'=>'rgba(244,124,134,.26)','tx'=>'#F47C86'],
],
'komoditas' => [
['label'=>'Kopi', 'total'=>148,'pct'=>60,'color'=>'#28a745','arc'=>174],
['label'=>'Kakao', 'total'=>82, 'pct'=>33,'color'=>'#007bff','arc'=>96],
['label'=>'Vanili','total'=>33, 'pct'=>13,'color'=>'#fd7e14','arc'=>38],
['label'=>'Kemiri','total'=>22, 'pct'=>9, 'color'=>'#6f42c1','arc'=>26],
['label'=>'Kopi', 'total'=>148,'pct'=>60,'color'=>'#6FD68F','arc'=>174],
['label'=>'Kakao', 'total'=>82, 'pct'=>33,'color'=>'#9D8EF0','arc'=>96],
['label'=>'Vanili','total'=>33, 'pct'=>13,'color'=>'#FFB46F','arc'=>38],
['label'=>'Kemiri','total'=>22, 'pct'=>9, 'color'=>'#9EA6B0','arc'=>26],
],
'kecamatan' => [
['name'=>'Bajawa','total'=>89,'pct'=>78,'color'=>'#28a745'],
['name'=>'Soa', 'total'=>68,'pct'=>59,'color'=>'#007bff'],
['name'=>'Golewa','total'=>57,'pct'=>50,'color'=>'#fd7e14'],
['name'=>'Bajawa','total'=>89,'pct'=>78,'color'=>'#6FD68F'],
['name'=>'Soa', 'total'=>68,'pct'=>59,'color'=>'#5FC7D6'],
['name'=>'Golewa','total'=>57,'pct'=>50,'color'=>'#FFB46F'],
],
'pending' => [
['name'=>'Theresia Nuka','section'=>'D4-K3','pct'=>67,'bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
['name'=>'Maria Seda', 'section'=>'D2-K2','pct'=>22,'bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
['name'=>'Rafael Bolo', 'section'=>'D2-K1','pct'=>11,'bg'=>'rgba(220,53,69,.15)', 'tx'=>'#dc3545'],
['name'=>'Antonius Wea', 'section'=>'D2-K1','pct'=>0, 'bg'=>'rgba(220,53,69,.15)', 'tx'=>'#dc3545'],
['name'=>'Theresia Nuka','section'=>'D4-K3','pct'=>67,'bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
['name'=>'Maria Seda', 'section'=>'D2-K2','pct'=>22,'bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
['name'=>'Rafael Bolo', 'section'=>'D2-K1','pct'=>11,'bg'=>'rgba(244,124,134,.26)', 'tx'=>'#F47C86'],
['name'=>'Antonius Wea', 'section'=>'D2-K1','pct'=>0, 'bg'=>'rgba(244,124,134,.26)', 'tx'=>'#F47C86'],
],
'pending_more' => 32,
];
@@ -258,10 +258,10 @@
@foreach($d['recent'] as $s)
@php
$bc = match(true) {
$s['pct'] === 100 => '#28a745',
$s['pct'] >= 50 => '#007bff',
$s['pct'] >= 20 => '#fd7e14',
default => '#dc3545',
$s['pct'] === 100 => '#6FD68F',
$s['pct'] >= 50 => '#5FC7D6',
$s['pct'] >= 20 => '#FFB46F',
default => '#F47C86',
};
@endphp
<tr>
+14 -10
View File
@@ -208,6 +208,10 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -225,7 +229,7 @@
{
label: 'Mentah/Gelondong (Rp)',
data: [35000, 200000, 25000, 15000],
backgroundColor: '#9ca3af', // Gray
backgroundColor: pastelSemantic.gray || '#9EA6B0',
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
barPercentage: 0.6,
categoryPercentage: 0.8
@@ -233,7 +237,7 @@
{
label: 'Produk Olahan (Rp)',
data: [120000, 1500000, 80000, 45000],
backgroundColor: '#d97706', // Amber-600
backgroundColor: pastelSemantic.warning || '#FFB46F',
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
barPercentage: 0.6,
categoryPercentage: 0.8
@@ -246,7 +250,7 @@
scales: {
y: {
beginAtZero: true,
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: {
callback: function(value) {
if (value >= 1000000) return 'Rp ' + (value/1000000) + ' Jt';
@@ -282,8 +286,8 @@
{
label: 'Kopi Bubuk/Roasted',
data: [12, 15, 18, 25, 30, 45],
borderColor: '#6F4E37',
backgroundColor: 'rgba(111, 78, 55, 0.1)',
borderColor: pastelCategory.coffee || '#6FD68F',
backgroundColor: 'rgba(104, 193, 124, 0.16)',
fill: true,
tension: 0.4,
borderWidth: 2
@@ -291,7 +295,7 @@
{
label: 'Kakao Fermentasi',
data: [5, 8, 12, 15, 22, 28],
borderColor: '#D2691E',
borderColor: pastelCategory.cacao || '#9D8EF0',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 2
@@ -303,9 +307,9 @@
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: {
title: { display: true, text: 'Volume (Ton)' },
grid: { color: '#f3f4f6' }
y: {
title: { display: true, text: 'Volume (Ton)' },
grid: { color: gridColor }
},
x: { grid: { display: false } }
},
@@ -318,4 +322,4 @@
});
</script>
@endpush
@endpush
@@ -60,8 +60,8 @@
}
.indicator-filter-bar .form-select:focus {
border-color: rgba(40, 167, 69, .55);
box-shadow: 0 0 0 .2rem rgba(40, 167, 69, .12);
border-color: rgba(111, 214, 143, .65);
box-shadow: 0 0 0 .2rem rgba(111, 214, 143, .20);
}
.indicator-filter-bar.is-loading .form-select,
@@ -190,7 +190,7 @@
.indicator-commodity-chip {
align-items: center;
background: linear-gradient(180deg, rgba(40, 199, 111, .08), rgba(40, 199, 111, .02));
background: rgba(111, 214, 143, .20);
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
display: flex;
@@ -201,7 +201,7 @@
.indicator-commodity-icon {
align-items: center;
background-color: rgba(40, 199, 111, .16);
background-color: rgba(111, 214, 143, .24);
border-radius: var(--bs-border-radius);
color: var(--bs-success);
display: inline-flex;
@@ -296,11 +296,11 @@
}
.bg-success-light {
background-color: rgba(40, 167, 69, .1);
background-color: rgba(104, 193, 124, .20);
}
.bg-primary-light {
background-color: rgba(0, 123, 255, .1);
background-color: rgba(95, 199, 214, .20);
}
.stat-value {
@@ -1658,7 +1658,7 @@
const parentDesa = (levels.desa || []).find(item => String(item.id) === String(parentId));
const desaLayer = parentDesa
? makeLayerFromPolygon(parentDesa.polygon, {
color: '#dc3545',
color: '#F47C86',
dashArray: '5, 8',
fillOpacity: 0,
interactive: false,
@@ -1673,8 +1673,8 @@
dataToRender.forEach(function (item) {
const layer = makeLayerFromPolygon(item.polygon, nextLevel === 'lahan'
? { color: '#28a745', fillColor: '#28a745', fillOpacity: .6, weight: 2 }
: { color: '#f39c12', fillColor: '#f1c40f', fillOpacity: .3, weight: 2 });
? { color: '#6FD68F', fillColor: '#6FD68F', fillOpacity: .6, weight: 2 }
: { color: '#FFB46F', fillColor: '#FFB46F', fillOpacity: .3, weight: 2 });
if (!layer) {
return;
@@ -1889,20 +1889,23 @@
}
const appConfig = typeof config !== 'undefined' ? config : {};
const pastelChart = appConfig.chartColors || {};
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
const pastelSemantic = pastelChart.semantic || {};
const headingColor = appConfig.colors?.headingColor || '#5d596c';
const legendColor = appConfig.colors?.bodyColor || '#6f6b7d';
const fontFamily = appConfig.fontFamily || 'Public Sans';
const chartColors = {
primary: appConfig.colors?.primary || '#7367f0',
success: appConfig.colors?.success || '#28c76f',
warning: appConfig.colors?.warning || '#ff9f43',
info: appConfig.colors?.info || '#00cfe8',
secondary: appConfig.colors?.secondary || '#a8aaae',
labelPrimary: appConfig.colors_label?.primary || 'rgba(115, 103, 240, 0.16)',
labelSuccess: appConfig.colors_label?.success || 'rgba(40, 199, 111, 0.16)',
labelWarning: appConfig.colors_label?.warning || 'rgba(255, 159, 67, 0.16)',
labelInfo: appConfig.colors_label?.info || 'rgba(0, 207, 232, 0.16)',
labelSecondary: appConfig.colors_label?.secondary || 'rgba(168, 170, 174, 0.16)',
primary: pastelSemantic.primary || '#9D8EF0',
success: pastelSemantic.success || '#6FD68F',
warning: pastelSemantic.warning || '#FFB46F',
info: pastelSemantic.info || '#5FC7D6',
secondary: pastelSemantic.secondary || '#9EA6B0',
labelPrimary: 'rgba(111, 214, 143, 0.22)',
labelSuccess: 'rgba(111, 214, 143, 0.24)',
labelWarning: 'rgba(255, 180, 111, 0.22)',
labelInfo: 'rgba(95, 199, 214, 0.20)',
labelSecondary: 'rgba(179, 181, 188, 0.22)',
body: appConfig.colors?.bodyColor || '#6f6b7d',
muted: appConfig.colors?.textMuted || '#a5a3ae',
heading: appConfig.colors?.headingColor || '#5d596c',
@@ -1910,12 +1913,7 @@
card: appConfig.colors?.cardColor || '#fff',
};
const surveyPalette = [
'#28a745',
'#007bff',
'#fd7e14',
'#dc3545',
'#6f42c1',
'#20c997',
...pastelSeries,
chartColors.primary,
chartColors.info,
chartColors.warning,
@@ -6,7 +6,7 @@
.chart-group-header-toggle {
cursor: pointer;
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.08);
border-inline-start: 4px solid var(--bs-primary, #7367f0);
border-inline-start: 4px solid var(--bs-primary, #9D8EF0);
box-shadow: none;
transition: background-color 0.2s ease;
}
@@ -649,18 +649,18 @@
<div class="col-12">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#demografi-charts"
aria-expanded="true" aria-controls="demografi-charts" aria-label="Sembunyikan chart">
aria-expanded="false" aria-controls="demografi-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Profil Demografi</h5>
<p class="text-body-secondary mb-0">Komposisi petani berdasarkan gender, kelompok umur, dan pendidikan.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-up"></i>
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse show chart-group-panel" id="demografi-charts">
<div class="collapse chart-group-panel" id="demografi-charts">
<div class="row g-6 pt-4">
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100">
@@ -729,18 +729,18 @@
<div class="col-12">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#rumah-tangga-charts"
aria-expanded="true" aria-controls="rumah-tangga-charts" aria-label="Sembunyikan chart">
aria-expanded="false" aria-controls="rumah-tangga-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Sosial Ekonomi</h5>
<p class="text-body-secondary mb-0">Tanggungan keluarga, penghasilan usaha tani, dan keanggotaan kelompok tani.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-up"></i>
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse show chart-group-panel" id="rumah-tangga-charts">
<div class="collapse chart-group-panel" id="rumah-tangga-charts">
<div class="row g-6 pt-4">
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100">
@@ -816,18 +816,18 @@
<div class="col-12">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#usaha-tani-charts"
aria-expanded="true" aria-controls="usaha-tani-charts" aria-label="Sembunyikan chart">
aria-expanded="false" aria-controls="usaha-tani-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Profil Lahan</h5>
<p class="text-body-secondary mb-0">Karakteristik lahan usaha tani berdasarkan komoditas utama, luas lahan garapan, dan status kepemilikan lahan.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-up"></i>
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse show chart-group-panel" id="usaha-tani-charts">
<div class="collapse chart-group-panel" id="usaha-tani-charts">
<div class="row g-6 pt-4">
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100">
@@ -1096,18 +1096,18 @@
<div class="py-4">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#indeks-segmentasi-charts"
aria-expanded="true" aria-controls="indeks-segmentasi-charts" aria-label="Sembunyikan chart">
aria-expanded="false" aria-controls="indeks-segmentasi-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Indeks Petani</h5>
<p class="text-body-secondary mb-0">Komposisi petani menurut tingkat kesiapan adopsi, secara keseluruhan dan pada setiap komoditas.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-up"></i>
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse show chart-group-panel" id="indeks-segmentasi-charts">
<div class="collapse chart-group-panel" id="indeks-segmentasi-charts">
<div class="row g-6 py-4">
<div class="col-lg-6 col-sm-12">
<div class="card card-border-shadow-primary h-100">
@@ -1169,18 +1169,18 @@
<div class="py-4">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#indeks-kesiapan-charts"
aria-expanded="true" aria-controls="indeks-kesiapan-charts" aria-label="Sembunyikan chart">
aria-expanded="false" aria-controls="indeks-kesiapan-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Dimensi dan Komponen Kesiapan Adopsi</h5>
<p class="text-body-secondary mb-0">Perbandingan skor dimensi dan komponen kesiapan adopsi petani.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-up"></i>
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse show chart-group-panel" id="indeks-kesiapan-charts">
<div class="collapse chart-group-panel" id="indeks-kesiapan-charts">
<div class="row g-6 py-4">
<div class="col-lg-6 col-sm-12">
<div class="card card-border-shadow-info h-100">
@@ -1219,18 +1219,18 @@
<div class="py-4">
<div class="card chart-group-header-toggle" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#indeks-wilayah-charts"
aria-expanded="true" aria-controls="indeks-wilayah-charts" aria-label="Sembunyikan chart">
aria-expanded="false" aria-controls="indeks-wilayah-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Sebaran wilayah petani</h5>
<p class="text-body-secondary mb-0">Peta dan komposisi segmen kesiapan adopsi petani per desa/kelurahan.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-up"></i>
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse show chart-group-panel" id="indeks-wilayah-charts">
<div class="collapse chart-group-panel" id="indeks-wilayah-charts">
<div class="row g-6 py-4">
<div class="col-lg-12 col-sm-12">
<div class="card card-border-shadow-danger h-100">
@@ -1366,6 +1366,32 @@ document.addEventListener('DOMContentLoaded', function () {
const headingColor = config.colors.headingColor;
const legendColor = config.colors.bodyColor;
const fontFamily = config.fontFamily;
const pastelChart = config.chartColors || {};
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
const pastelSemantic = pastelChart.semantic || {};
const pastelText = pastelChart.text || {};
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '');
if (!/^[0-9a-fA-F]{6}$/.test(value)) {
return hex;
}
const red = parseInt(value.slice(0, 2), 16);
const green = parseInt(value.slice(2, 4), 16);
const blue = parseInt(value.slice(4, 6), 16);
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function softBadgeStyle(backgroundColor, textColor) {
const borderColor = hexToRgba(backgroundColor, 0.36);
return [
`background-color:${hexToRgba(backgroundColor, 0.22)} !important`,
`border:1px solid ${borderColor}`,
`color:${textColor} !important`,
'font-weight:600'
].join(';');
}
const farmerDataUrl = @json(route('dashboard.demografi-petani.data'));
const farmerProfileUrl = @json(url('/dashboard/profil/petani'));
@@ -1500,8 +1526,8 @@ document.addEventListener('DOMContentLoaded', function () {
tbody.innerHTML = rows.map((petani, index) => {
const rowNumber = ((result.pagination.current_page - 1) * result.pagination.per_page) + index + 1;
const genderBadge = Number(petani.jenis_kelamin) === 1
? `<span class="badge bg-label-info">Laki-laki</span>`
: `<span class="badge" style="background-color:#ffe8f1 !important; color:#ff5598 !important;">Perempuan</span>`;
? `<span class="badge" style="${softBadgeStyle(pastelSemantic.info || '#5FC7D6', pastelText.info || '#00BAD1')}">Laki-laki</span>`
: `<span class="badge" style="${softBadgeStyle(pastelSemantic.danger || '#F47C86', pastelText.danger || '#FF4C51')}">Perempuan</span>`;
const umur = (petani.umur !== null && petani.umur !== undefined) ? `${petani.umur} tahun` : '-';
return `
@@ -1706,7 +1732,7 @@ document.addEventListener('DOMContentLoaded', function () {
},
labels: ['Laki-laki', 'Perempuan'],
series: [lakiLaki, perempuan],
colors: [config.colors.info, '#ff5598'],
colors: [pastelSemantic.info || '#5FC7D6', pastelSemantic.pink || '#F47C86'],
stroke: { width: 0 },
dataLabels: {
enabled: true,
@@ -1815,12 +1841,11 @@ document.addEventListener('DOMContentLoaded', function () {
data: ageGroupData
}
],
colors: ['#c7c2f9', '#9d94f4', '#7367f0', '#5048a8', '#2e2960'],
colors: [pastelSemantic.primary || '#9D8EF0'],
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: '45%',
distributed: true,
dataLabels: { position: 'top' }
}
},
@@ -1875,7 +1900,7 @@ document.addEventListener('DOMContentLoaded', function () {
const label = w.globals.labels[dataPointIndex];
const percentage = totalAgeGroup > 0 ? (Number(value) / totalAgeGroup) * 100 : 0;
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:#fff; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
${label} tahun: <strong>${value} petani (${percentage.toFixed(1)}%)</strong>
</div>
`;
@@ -1918,10 +1943,9 @@ document.addEventListener('DOMContentLoaded', function () {
}
},
series: [{ name: 'Petani', data: educationData }],
colors: ['#e9f9f0', '#a9e8c5', '#68d79a', '#28c76f', '#1c8b4d', '#104f2c'],
colors: [pastelSemantic.success || '#6FD68F'],
plotOptions: {
bar: {
distributed: true,
horizontal: true,
borderRadius: 4,
barHeight: '55%',
@@ -1975,7 +1999,7 @@ document.addEventListener('DOMContentLoaded', function () {
const label = w.globals.labels[dataPointIndex];
const percentage = totalPendidikan > 0 ? ((value / totalPendidikan) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:#fff; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
@@ -2017,10 +2041,9 @@ document.addEventListener('DOMContentLoaded', function () {
}
},
series: [{ name: 'Petani', data: dependentData }],
colors: ['#ffb7b9', '#ff4c51', '#b23538', '#661e20'],
colors: [pastelSemantic.danger || '#F47C86'],
plotOptions: {
bar: {
distributed: true,
horizontal: true,
borderRadius: 4,
barHeight: '50%',
@@ -2074,7 +2097,7 @@ document.addEventListener('DOMContentLoaded', function () {
const label = w.globals.labels[dataPointIndex];
const percentage = totalDependentsFarmers > 0 ? ((value / totalDependentsFarmers) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:#fff; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
@@ -2116,7 +2139,7 @@ document.addEventListener('DOMContentLoaded', function () {
},
labels: ['Terdaftar', 'Tidak Terdaftar', 'Belum Terisi'],
series: farmerGroupData,
colors: [config.colors.success, config.colors.warning, config.colors.secondary],
colors: [pastelSemantic.success || '#6FD68F', pastelSemantic.warning || '#FFB46F', pastelSemantic.secondary || '#9EA6B0'],
stroke: { width: 0 },
dataLabels: {
enabled: true,
@@ -2215,12 +2238,12 @@ document.addEventListener('DOMContentLoaded', function () {
labels: commodityLabels,
series: commoditySeries.map(Number),
colors: [
config.colors.primary,
config.colors.warning,
config.colors.success,
config.colors.info,
config.colors.danger,
config.colors.secondary
pastelSemantic.primary || '#9D8EF0',
pastelSemantic.warning || '#FFB46F',
pastelSemantic.success || '#6FD68F',
pastelSemantic.info || '#5FC7D6',
pastelSemantic.danger || '#F47C86',
pastelSemantic.secondary || '#9EA6B0'
],
stroke: { width: 0 },
dataLabels: {
@@ -2270,7 +2293,7 @@ document.addEventListener('DOMContentLoaded', function () {
const percentage = totalCommodityFarmers > 0 ? ((value / totalCommodityFarmers) * 100).toFixed(1) : '0.0';
const color = w.globals.colors[seriesIndex];
return `
<div style="background:${color}; color:#fff; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
<div style="background:${color}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
@@ -2335,10 +2358,9 @@ document.addEventListener('DOMContentLoaded', function () {
}
},
series: [{ name: 'Petani', data: landAreaData }],
colors: ['#ffbb7b', '#ff9f43', '#b26f2e', '#663f1a'],
colors: [pastelSemantic.warning || '#FFB46F'],
plotOptions: {
bar: {
distributed: true,
horizontal: true,
borderRadius: 4,
barHeight: '50%',
@@ -2392,7 +2414,7 @@ document.addEventListener('DOMContentLoaded', function () {
const label = w.globals.labels[dataPointIndex];
const percentage = totalLandFarmers > 0 ? ((value / totalLandFarmers) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:#fff; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
@@ -2431,10 +2453,10 @@ document.addEventListener('DOMContentLoaded', function () {
labels: landOwnershipStatusLabels,
series: landOwnershipStatusSeries,
colors: [
config.colors.success,
config.colors.warning,
config.colors.info,
config.colors.secondary
pastelSemantic.success || '#6FD68F',
pastelSemantic.warning || '#FFB46F',
pastelSemantic.info || '#5FC7D6',
pastelSemantic.secondary || '#9EA6B0'
],
stroke: { width: 0 },
dataLabels: {
@@ -2538,10 +2560,9 @@ document.addEventListener('DOMContentLoaded', function () {
}
},
series: [{ name: 'Petani', data: incomeData }],
colors: ['#99e3ec', '#4ccede', '#00bad1', '#008292', '#004a53'],
colors: [pastelSemantic.info || '#5FC7D6'],
plotOptions: {
bar: {
distributed: true,
horizontal: true,
borderRadius: 4,
barHeight: '50%',
@@ -2595,7 +2616,7 @@ document.addEventListener('DOMContentLoaded', function () {
const label = w.globals.labels[dataPointIndex];
const percentage = totalIncomeFarmers > 0 ? ((value / totalIncomeFarmers) * 100).toFixed(1) : '0.0';
return `
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:#fff; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
<div style="background:${w.config.colors[dataPointIndex % w.config.colors.length]}; color:${headingColor}; padding:8px 12px; border-radius:5px; font-size:13px; font-weight:500; font-family:${fontFamily}; box-shadow:0 4px 12px rgba(0,0,0,.15);">
${label}: <strong>${value} petani (${percentage}%)</strong>
</div>
`;
@@ -2832,6 +2853,10 @@ document.addEventListener('DOMContentLoaded', function () {
<script>
$(document).ready(function () {
const segments = @json($segments);
const segmentPalette = segments.map(function (segment, index) {
return config.chartColors?.semantic?.[segment.color] || config.chartColors?.series?.[index % config.chartColors.series.length] || '#9EA6B0';
});
const segmentTextColors = config.chartColors?.text || {};
// Chart Segmentasi Petani
var chartSegmentasiPetani;
@@ -2871,7 +2896,7 @@ document.addEventListener('DOMContentLoaded', function () {
}
},
labels: @json(collect($segments)->pluck('name')->values()),
colors: @json($colors),
colors: segmentPalette,
plotOptions: {
pie: {
borderRadius: 12,
@@ -2925,16 +2950,17 @@ document.addEventListener('DOMContentLoaded', function () {
const total = response.series[index] ?? 0;
const persen = response.persentase[index] ?? 0;
const segmentColor = segmentTextColors[segment.color] || config.colors[segment.color] || segmentPalette[index] || '#808390';
html += `
<tr>
<td>
<div class="d-flex justify-content-start align-items-center gap-2">
<div class="flex-shrink-0">
<i class="icon-base ti ${segment.icon} icon-lg text-${segment.color}"></i>
<i class="icon-base ti ${segment.icon} icon-lg" style="color:${segmentColor};"></i>
</div>
<span class="text-${segment.color}">
<span style="color:${segmentColor};">
${segment.name}
</span>
</div>
@@ -2971,9 +2997,12 @@ document.addEventListener('DOMContentLoaded', function () {
</script>
<script>
$(document).ready(function () {
const segmentPalette = (@json($segments)).map(function (segment, index) {
return config.chartColors?.semantic?.[segment.color] || config.chartColors?.series?.[index % config.chartColors.series.length] || '#9EA6B0';
});
var optionsSegmentasiPetaniByKomoditas = {
series: [],
colors: @json($colors),
colors: segmentPalette,
chart: {
type: 'bar',
@@ -3157,7 +3186,7 @@ document.addEventListener('DOMContentLoaded', function () {
plotOptions: { radar: { polygons: { strokeColors: borderColor, connectorColors: borderColor } } },
stroke: { width: 2 },
legend: { show: false },
colors: [config.colors.primary],
colors: [config.chartColors?.semantic?.primary || '#9D8EF0'],
fill: { opacity: 0.12 },
markers: {
size: 5, strokeWidth: 2, strokeColors: config.colors.cardColor, hover: { sizeOffset: 2 },
@@ -3208,7 +3237,14 @@ document.addEventListener('DOMContentLoaded', function () {
</script>
<script>
$(document).ready(function () {
var palette = [config.colors.success, config.colors.primary, config.colors.warning, config.colors.info, config.colors.danger, config.colors.secondary];
var palette = [
config.chartColors?.semantic?.success || '#6FD68F',
config.chartColors?.semantic?.primary || '#9D8EF0',
config.chartColors?.semantic?.warning || '#FFB46F',
config.chartColors?.semantic?.info || '#5FC7D6',
config.chartColors?.semantic?.danger || '#F47C86',
config.chartColors?.semantic?.secondary || '#9EA6B0'
];
[
{ id: 'dimensi', key: 'radarDimensi', url: "{{ route('dashboard.indeks-petani.dimensi') }}", title: "Dimensi Kesiapan Adopsi", labels: ["Sistem & Praktik Budidaya","Sosial Ekonomi Rumah Tangga","Rantai Nilai & Akses Pasar"], categories: [["Sistem & Praktik","Budidaya"],["Sosial Ekonomi","Rumah Tangga"],["Rantai Nilai &","Akses Pasar"]] },
{ id: 'komponen', key: 'radarKomponen', url: "{{ route('dashboard.indeks-petani.komponen') }}", title: "Komponen Kesiapan Adopsi", labels: ["Motivasi & Kapasitas Adopsi","Kondisi Ekonomi Rumah Tangga","Praktik Budidaya & GAP","Keterlibatan Kelembagaan","Akses & Kebebasan Keuangan","Rantai Nilai & Akses Pasar"], categories: [["Motivasi & Kapasitas","Adopsi"],["Kondisi Ekonomi","Rumah Tangga"],["Praktik Budidaya","& GAP"],["Keterlibatan","Kelembagaan"],["Akses & Kebebasan","Keuangan"],["Rantai Nilai &","Akses Pasar"]] }
@@ -3282,7 +3318,7 @@ document.addEventListener('DOMContentLoaded', function () {
const warnaSegmen = {
@foreach($segments as $segment)
"{{ $segment['name'] }}": "var(--bs-{{ $segment['color'] }})",
"{{ $segment['name'] }}": config.chartColors?.semantic?.["{{ $segment['color'] }}"] || '#9EA6B0',
@endforeach
};
@@ -3340,7 +3376,7 @@ document.addEventListener('DOMContentLoaded', function () {
color: '#666',
weight: 1,
fillColor:
warnaSegmen[desa.segmentasi?.segmen] || '#ccc',
warnaSegmen[desa.segmentasi?.segmen] || '#9EA6B0',
fillOpacity: 0.7
});
@@ -3389,6 +3425,7 @@ document.addEventListener('DOMContentLoaded', function () {
</script>
<script>
$(document).ready(function () {
const segmentTextColors = config.chartColors?.text || {};
var tableDesa = $('#table-desa').DataTable({
"paging": true,
"pageLength": 5,
@@ -3429,7 +3466,8 @@ document.addEventListener('DOMContentLoaded', function () {
var count = Number(data && data.count) || 0;
if (type !== 'display') return count;
var percentage = Number(data && data.percentage || 0).toLocaleString('id-ID', { maximumFractionDigits: 2 });
return '<span class="fw-semibold text-' + (data && data.color || 'secondary') + '">' + count.toLocaleString('id-ID') +
var segmentColor = segmentTextColors[data && data.color || 'secondary'] || '#808390';
return '<span class="fw-semibold" style="color:' + segmentColor + ';">' + count.toLocaleString('id-ID') +
'</span> <span class="text-body-secondary small">(' + percentage + '%)</span>';
}
};
@@ -3439,6 +3477,36 @@ document.addEventListener('DOMContentLoaded', function () {
</script>
<script>
$(document).ready(function () {
const indexSegmentColorKeys = {
@foreach($segments as $segment)
"{{ $segment['name'] }}": "{{ $segment['color'] }}",
@endforeach
};
const indexSegmentColors = {
@foreach($segments as $segment)
"{{ $segment['name'] }}": config.chartColors?.semantic?.["{{ $segment['color'] }}"] || '#9EA6B0',
@endforeach
};
const indexSegmentTextColors = config.chartColors?.text || {};
function indexHexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '');
if (!/^[0-9a-fA-F]{6}$/.test(value)) {
return hex;
}
const red = parseInt(value.slice(0, 2), 16);
const green = parseInt(value.slice(2, 4), 16);
const blue = parseInt(value.slice(4, 6), 16);
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function indexSegmentBadgeStyle(backgroundColor, textColor) {
return [
`background-color:${indexHexToRgba(backgroundColor, 0.22)} !important`,
`border:1px solid ${indexHexToRgba(backgroundColor, 0.36)}`,
`color:${textColor} !important`,
'font-weight:600'
].join(';');
}
var tablePetani = $('#table-petani').DataTable({
dom: 'rt<"card-footer d-flex flex-wrap justify-content-between align-items-center gap-3"ip>',
pagingType: 'full_numbers',
@@ -3540,9 +3608,12 @@ document.addEventListener('DOMContentLoaded', function () {
if (!data.skor || !data.segmen) {
return '';
}
const segmentKey = indexSegmentColorKeys[data.segmen] || data.color || 'secondary';
const segmentColor = indexSegmentColors[data.segmen] || config.chartColors?.semantic?.[segmentKey] || '#9EA6B0';
const segmentTextColor = indexSegmentTextColors[segmentKey] || '#808390';
return `
<span class="badge bg-${data.color ?? 'secondary'}">
<span class="badge" style="${indexSegmentBadgeStyle(segmentColor, segmentTextColor)}">
${data.segmen}
</span>
`;
@@ -212,6 +212,9 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -230,8 +233,8 @@
label: 'Total Produksi Lahan (Ton)',
type: 'line',
data: [100, 110, 125, 145],
borderColor: '#10b981', // Emerald-500
backgroundColor: '#10b981',
borderColor: pastelCategory.marketGreen || '#6FD68F',
backgroundColor: pastelCategory.marketGreen || '#6FD68F',
borderWidth: 3,
tension: 0.3,
fill: false
@@ -239,14 +242,14 @@
{
label: 'Tengkulak / Pasar Bebas',
data: [80, 65, 40, 25],
backgroundColor: '#ef4444', // Red-500
backgroundColor: pastelCategory.marketRed || '#F47C86',
stack: 'Stack 0',
borderRadius: { topLeft: 0, topRight: 0, bottomLeft: 4, bottomRight: 4 }
},
{
label: 'Kelembagaan (Koperasi/BUMDes)',
data: [20, 45, 85, 120],
backgroundColor: '#3b82f6', // Blue-500
backgroundColor: pastelCategory.marketBlue || '#5FC7D6',
stack: 'Stack 0',
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 }
}
@@ -260,7 +263,7 @@
y: {
stacked: true,
beginAtZero: true,
grid: { color: '#f3f4f6' }
grid: { color: gridColor }
},
x: {
stacked: true,
@@ -292,20 +295,20 @@
{
label: 'Kopi & Kakao',
data: [150, 280, 320, 500, 650, 850],
borderColor: '#6F4E37',
borderColor: pastelCategory.coffee || '#6FD68F',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 2,
pointBackgroundColor: '#6F4E37'
pointBackgroundColor: pastelCategory.coffee || '#6FD68F'
},
{
label: 'Vanili & Kemiri',
data: [50, 120, 210, 350, 420, 600],
borderColor: '#eab308',
borderColor: pastelCategory.vanilla || '#FFB46F',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 2,
pointBackgroundColor: '#eab308'
pointBackgroundColor: pastelCategory.vanilla || '#FFB46F'
}
]
},
@@ -316,7 +319,7 @@
scales: {
y: {
title: { display: false },
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: {
callback: function(value) {
return 'Rp ' + value + ' Jt';
@@ -341,4 +344,4 @@
});
</script>
@endpush
@endpush
+3 -2
View File
@@ -493,6 +493,7 @@ $segments = [
legendColor = config.colors.bodyColor;
borderColor = config.colors.borderColor;
headingColor = config.colors.headingColor;
const pastelSemantic = config.chartColors?.semantic || {};
var radarDimensi = new ApexCharts($("#chart-dimensi")[0],
options = {
@@ -542,7 +543,7 @@ $segments = [
highlightDataSeries: false
}
},
colors: [config.colors.primary, config.colors.info],
colors: [pastelSemantic.primary || '#9D8EF0', pastelSemantic.info || '#5FC7D6'],
fill: {
opacity: 0.85
},
@@ -640,7 +641,7 @@ $segments = [
highlightDataSeries: false
}
},
colors: [config.colors.primary, config.colors.info],
colors: [pastelSemantic.primary || '#9D8EF0', pastelSemantic.info || '#5FC7D6'],
fill: {
opacity: 0.85
},
+408 -65
View File
@@ -2,21 +2,85 @@
@push('style')
<style>
.chart-group-header-toggle {
cursor: pointer;
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.08);
border-inline-start: 4px solid var(--bs-primary, #9D8EF0);
box-shadow: none;
transition: background-color 0.2s ease;
}
.chart-group-header-toggle:hover {
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.13);
}
.chart-group-header-toggle .card-body {
padding: 12px 16px;
}
.chart-group-header-toggle h5 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 2px;
}
.chart-group-header-toggle p {
font-size: 0.8125rem;
line-height: 1.4;
}
.chart-group-header-toggle:focus-visible {
outline: 2px solid var(--bs-primary);
outline-offset: 3px;
}
.chart-group-icon-toggle {
width: 34px;
height: 34px;
flex-shrink: 0;
}
.chart-group-icon-toggle .icon-base {
font-size: 24px;
width: 24px;
height: 24px;
}
.chart-group-panel.show,
.chart-group-panel.collapsing {
margin-bottom: 1.5rem;
}
.produksi-kpi-value {
font-size: 1.35rem;
font-weight: 800;
line-height: 1.1;
color: var(--bs-heading-color);
font-size: 1.6rem;
font-weight: 600;
line-height: 1.2;
overflow-wrap: anywhere;
}
.produksi-kpi-label {
font-size: .82rem;
font-weight: 700;
margin-bottom: .2rem;
color: var(--bs-heading-color);
font-size: 0.9375rem;
font-weight: 400;
}
.produksi-kpi-desc {
font-size: .78rem;
font-size: 0.8125rem;
line-height: 1.4;
}
.produksi-chart-card .card-title h5 {
color: var(--bs-heading-color);
font-size: 1.125rem;
font-weight: 500;
line-height: 1.35;
letter-spacing: 0;
}
.produksi-chart-card .card-title small {
font-size: 0.8125rem;
line-height: 1.4;
}
.produksi-chart-sm {
@@ -31,6 +95,52 @@
min-height: 240px;
}
.produksi-praktik-chart {
min-height: 360px;
}
.produksi-pie-canvas {
min-height: 190px;
}
.produksi-pie-legend {
display: flex;
flex-direction: column;
gap: .5rem;
margin-top: .75rem;
}
.produksi-pie-legend-item {
display: flex;
align-items: flex-start;
gap: .5rem;
font-size: 0.8125rem;
line-height: 1.4;
color: var(--bs-body-color);
}
.produksi-pie-legend-marker {
width: .65rem;
height: .65rem;
border-radius: 999px;
flex: 0 0 .65rem;
margin-top: .24rem;
}
.produksi-pie-legend-label {
min-width: 0;
flex: 1 1 auto;
overflow-wrap: anywhere;
}
.produksi-pie-legend-value {
flex: 0 0 auto;
color: var(--bs-heading-color);
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
}
.produksi-empty {
min-height: 220px;
display: flex;
@@ -142,7 +252,7 @@
<div class="row g-4 mb-4">
@foreach ($kpiCards as $card)
<div class="col-xl-4 col-md-4">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100">
<div class="card card-border-shadow-{{ $card['variant'] }} h-100 produksi-kpi-card">
<div class="card-body">
<div class="d-flex align-items-center gap-4">
<div class="avatar avatar-lg">
@@ -159,9 +269,9 @@
@endif
</div>
<p class="mb-0 mt-1">{{ $card['label'] }}</p>
<p class="mb-0 mt-1 produksi-kpi-label">{{ $card['label'] }}</p>
<small class="text-body-secondary">{{ $card['description'] }}</small>
<small class="text-body-secondary produksi-kpi-desc">{{ $card['description'] }}</small>
</div>
</div>
</div>
@@ -170,33 +280,29 @@
@endforeach
</div>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-start gap-2 mb-3">
<div>
<h5 class="mb-1">Kondisi Kebun</h5>
<p class="text-body-secondary mb-0">Ringkasan umur lahan, umur tanaman, kondisi tanaman, gejala terlihat, dan tingkat keparahan serangan.</p>
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#produksi-charts"
aria-expanded="false" aria-controls="produksi-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Lahan dan Tanaman</h5>
<p class="text-body-secondary mb-0">Distribusi umur tanaman, jumlah tanaman per komoditas, kondisi tanaman, gejala serangan, dan tingkat keparahan serangan.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
<button type="button" class="btn btn-label-secondary btn-sm chart-group-toggle" data-bs-toggle="collapse" data-bs-target="#produksi-charts" aria-expanded="true" aria-controls="produksi-charts">Sembunyikan Chart</button>
</div>
<div class="collapse show chart-group-panel" id="produksi-charts">
<div class="collapse chart-group-panel" id="produksi-charts">
<div class="row g-4 mb-4">
<div class="col-xl-4 col-lg-6">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold">Umur Lahan</h6>
<small class="text-body-secondary">Kelompok umur lahan terdata</small>
</div>
<div class="card-body">
<div id="chart-umur-lahan" class="produksi-chart-sm"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold">Umur Tanaman</h6>
<small class="text-body-secondary">Kelompok umur tanaman saat ini</small>
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Umur Tanaman</h5>
<small class="text-body-secondary">Kelompok umur tanaman saat ini</small>
</div>
</div>
<div class="card-body">
<div id="chart-umur-tanaman" class="produksi-chart-sm"></div>
@@ -205,10 +311,26 @@
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold">Kondisi Tanaman</h6>
<small class="text-body-secondary">Distribusi kondisi baik, sedang, buruk, atau mati</small>
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Jumlah Tanaman Per Komoditas</h5>
<small class="text-body-secondary">Jumlah tanaman terdata pada setiap komoditas</small>
</div>
</div>
<div class="card-body">
<div id="chart-jumlah-tanaman-komoditas" class="produksi-chart-sm"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Kondisi Tanaman</h5>
<small class="text-body-secondary">Distribusi kondisi baik, sedang, buruk, atau mati</small>
</div>
</div>
<div class="card-body">
<div id="chart-kondisi-tanaman" class="produksi-chart"></div>
@@ -219,10 +341,12 @@
<div class="row g-4">
<div class="col-xl-8 col-lg-8">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold">Gejala Serangan yang Terlihat</h6>
<small class="text-body-secondary">Gabungan data hama, penyakit, dan gejala otomatis</small>
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Gejala Serangan yang Terlihat</h5>
<small class="text-body-secondary">Gabungan data hama, penyakit, dan gejala otomatis</small>
</div>
</div>
<div class="card-body">
<div id="chart-gejala-serangan" class="produksi-chart-gejala"></div>
@@ -231,10 +355,12 @@
</div>
<div class="col-xl-4 col-lg-4">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold">Keparahan Serangan</h6>
<small class="text-body-secondary">Ringan, sedang, dan parah</small>
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Keparahan Serangan</h5>
<small class="text-body-secondary">Ringan, sedang, dan parah</small>
</div>
</div>
<div class="card-body">
<div id="chart-keparahan-serangan" class="produksi-chart"></div>
@@ -243,6 +369,111 @@
</div>
</div>
</div>
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#praktik-budidaya-charts"
aria-expanded="false" aria-controls="praktik-budidaya-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Praktik Budidaya</h5>
<p class="text-body-secondary mb-0">Distribusi intensitas pemangkasan, pemupukan, penanganan hama dan penyakit, pembersihan lahan, sumber air, dan kebiasaan praktik baik.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse chart-group-panel" id="praktik-budidaya-charts">
<div class="row g-4 mb-4">
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Intensitas Pemangkasan</h5>
<small class="text-body-secondary">Frekuensi dan ketepatan teknik pemangkasan</small>
</div>
</div>
<div class="card-body">
<div id="chart-intensitas-pemangkasan" class="produksi-praktik-chart"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Intensitas Pemupukan</h5>
<small class="text-body-secondary">Kesesuaian jenis, dosis, dan waktu pemupukan</small>
</div>
</div>
<div class="card-body">
<div id="chart-intensitas-pemupukan" class="produksi-praktik-chart"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Metode Penanganan Hama dan Penyakit</h5>
<small class="text-body-secondary">Cara penanganan serangan yang dilakukan petani</small>
</div>
</div>
<div class="card-body">
<div id="chart-metode-penanganan-hama-penyakit" class="produksi-praktik-chart"></div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Intensitas Pembersihan Lahan</h5>
<small class="text-body-secondary">Kebiasaan sanitasi dan pembersihan kebun</small>
</div>
</div>
<div class="card-body">
<div id="chart-intensitas-pembersihan-lahan" class="produksi-praktik-chart"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Sumber Air untuk Kebun</h5>
<small class="text-body-secondary">Sumber air yang digunakan di kebun</small>
</div>
</div>
<div class="card-body">
<div id="chart-sumber-air-kebun" class="produksi-praktik-chart"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="card h-100 produksi-chart-card">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Kebiasaan Praktik Baik</h5>
<small class="text-body-secondary">Praktik konservasi sederhana yang dilakukan petani</small>
</div>
</div>
<div class="card-body">
<div id="chart-kebiasaan-praktik-baik" class="produksi-praktik-chart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -251,9 +482,14 @@
document.addEventListener('DOMContentLoaded', function () {
const chartData = @json($chartData);
const wilayah = @json($wilayah);
const chartColors = ['#28a745', '#007bff', '#fd7e14', '#dc3545', '#6f42c1', '#20c997', '#6c757d', '#17a2b8'];
const labelColor = '#6c757d';
const borderColor = 'rgba(128, 128, 128, .18)';
const headingColor = config.colors.headingColor;
const legendColor = config.colors.bodyColor;
const fontFamily = config.fontFamily;
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const chartColors = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
const labelColor = legendColor;
const borderColor = pastelChart.grid || 'rgba(128, 128, 128, .18)';
function hasData(data) {
return data && Array.isArray(data.series) && data.series.some(function (value) {
@@ -285,7 +521,7 @@ document.addEventListener('DOMContentLoaded', function () {
}, 0);
new ApexCharts(element, {
chart: { type: 'donut', height: 265, toolbar: { show: false } },
chart: { type: 'donut', height: 265, fontFamily: fontFamily, toolbar: { show: false } },
labels: data.labels,
series: data.series.map(Number),
colors: chartColors,
@@ -296,10 +532,19 @@ document.addEventListener('DOMContentLoaded', function () {
formatter: function (value) {
return value.toFixed(0) + '%';
},
style: {
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 600,
colors: ['#fff'],
},
},
legend: {
position: 'bottom',
labels: { colors: labelColor },
fontSize: '13px',
fontFamily: fontFamily,
fontWeight: 400,
formatter: function (seriesName, options) {
const value = Number(options.w.globals.series[options.seriesIndex] || 0);
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
@@ -307,6 +552,7 @@ document.addEventListener('DOMContentLoaded', function () {
},
},
tooltip: {
theme: 'dark',
y: {
formatter: function (value) {
const percentage = total > 0 ? ((Number(value) / total) * 100).toFixed(1) : '0.0';
@@ -334,6 +580,76 @@ document.addEventListener('DOMContentLoaded', function () {
}).render();
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function renderPieWithLegend(selector, data, options = {}) {
const element = document.querySelector(selector);
if (!element) return;
if (!hasData(data)) return emptyChart(selector);
const series = data.series.map(Number);
const total = series.reduce(function (sum, value) {
return sum + Number(value || 0);
}, 0);
const colors = options.colors || chartColors;
element.innerHTML = '<div class="produksi-pie-canvas"></div><div class="produksi-pie-legend"></div>';
const canvas = element.querySelector('.produksi-pie-canvas');
const legend = element.querySelector('.produksi-pie-legend');
legend.innerHTML = data.labels.map(function (label, index) {
const value = Number(series[index] || 0);
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
const color = colors[index % colors.length];
return `
<div class="produksi-pie-legend-item">
<span class="produksi-pie-legend-marker" style="background-color: ${escapeHtml(color)}"></span>
<span class="produksi-pie-legend-label">${escapeHtml(label)}</span>
<span class="produksi-pie-legend-value">${formatNumber(value)} (${percentage}%)</span>
</div>
`;
}).join('');
new ApexCharts(canvas, {
chart: { type: 'pie', height: options.height || 205, fontFamily: fontFamily, toolbar: { show: false } },
labels: data.labels,
series: series,
colors: colors,
stroke: { width: 0 },
legend: { show: false },
dataLabels: {
enabled: true,
formatter: function (value) {
return value >= 8 ? value.toFixed(0) + '%' : '';
},
style: {
fontSize: '11px',
fontFamily: fontFamily,
fontWeight: 600,
colors: ['#fff'],
},
},
tooltip: {
theme: 'dark',
y: {
formatter: function (value) {
const percentage = total > 0 ? ((Number(value) / total) * 100).toFixed(1) : '0.0';
return `${formatNumber(value)} ${options.unit || 'data'} (${percentage}%)`;
},
},
},
plotOptions: { pie: { expandOnClick: true } },
}).render();
}
function renderBar(selector, data, options = {}) {
const element = document.querySelector(selector);
if (!element) return;
@@ -345,11 +661,11 @@ document.addEventListener('DOMContentLoaded', function () {
}, 0);
new ApexCharts(element, {
chart: { type: 'bar', height: options.height || 285, toolbar: { show: false } },
chart: { type: 'bar', height: options.height || 285, fontFamily: fontFamily, toolbar: { show: false } },
series: [{ name: options.name || 'Jumlah', data: data.series.map(Number) }],
xaxis: {
categories: data.labels,
labels: { style: { colors: labelColor } },
labels: { style: { colors: labelColor, fontSize: '12px', fontFamily: fontFamily } },
axisBorder: { color: borderColor },
axisTicks: { color: borderColor },
},
@@ -357,10 +673,10 @@ document.addEventListener('DOMContentLoaded', function () {
labels: {
minWidth: options.yLabelWidth || 0,
maxWidth: options.yLabelWidth || 160,
style: { colors: labelColor },
style: { colors: labelColor, fontSize: '12px', fontFamily: fontFamily },
},
},
colors: [options.color || '#28a745'],
colors: [options.color || pastelSemantic.success || '#6FD68F'],
plotOptions: {
bar: {
horizontal: horizontal,
@@ -377,8 +693,9 @@ document.addEventListener('DOMContentLoaded', function () {
offsetY: horizontal ? 0 : -20,
style: {
fontSize: '12px',
fontFamily: fontFamily,
fontWeight: 500,
colors: [labelColor],
colors: [headingColor],
},
formatter: function (value) {
const percentage = total > 0 ? ((Number(value) / total) * 100).toFixed(1) : '0.0';
@@ -394,6 +711,7 @@ document.addEventListener('DOMContentLoaded', function () {
},
},
tooltip: {
theme: 'dark',
y: {
formatter: function (value) {
const percentage = total > 0 ? ((Number(value) / total) * 100).toFixed(1) : '0.0';
@@ -439,11 +757,17 @@ document.addEventListener('DOMContentLoaded', function () {
element.innerHTML = '<div class="produksi-empty">ApexCharts belum dimuat.</div>';
});
} else {
renderBar('#chart-umur-lahan', chartData.umurLahan, { color: '#007bff', unit: 'lahan' });
renderBar('#chart-umur-tanaman', chartData.umurTanaman, { color: '#fd7e14', unit: 'data' });
renderBar('#chart-umur-tanaman', chartData.umurTanaman, { color: pastelSemantic.warning || '#FFB46F', unit: 'data' });
renderBar('#chart-jumlah-tanaman-komoditas', chartData.jumlahTanamanPerKomoditas, { color: pastelSemantic.success || '#6FD68F', unit: 'tanaman' });
renderDonut('#chart-kondisi-tanaman', chartData.kondisiTanaman);
renderBar('#chart-gejala-serangan', chartData.gejalaSerangan, { horizontal: true, height: 250, color: '#dc3545', unit: 'data', yLabelWidth: 210, rightPadding: 115 });
renderBar('#chart-gejala-serangan', chartData.gejalaSerangan, { horizontal: true, height: 250, color: pastelSemantic.danger || '#F47C86', unit: 'data', yLabelWidth: 210, rightPadding: 115 });
renderDonut('#chart-keparahan-serangan', chartData.keparahanSerangan);
renderPieWithLegend('#chart-intensitas-pemangkasan', chartData.intensitasPemangkasan, { unit: 'petani' });
renderPieWithLegend('#chart-intensitas-pemupukan', chartData.intensitasPemupukan, { unit: 'petani' });
renderPieWithLegend('#chart-metode-penanganan-hama-penyakit', chartData.metodePenangananHamaPenyakit, { unit: 'petani' });
renderPieWithLegend('#chart-intensitas-pembersihan-lahan', chartData.intensitasPembersihanLahan, { unit: 'petani' });
renderPieWithLegend('#chart-sumber-air-kebun', chartData.sumberAirKebun, { unit: 'petani' });
renderPieWithLegend('#chart-kebiasaan-praktik-baik', chartData.kebiasaanPraktikBaik, { unit: 'petani' });
}
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
@@ -493,16 +817,35 @@ document.addEventListener('DOMContentLoaded', function () {
window.location.href = @json(route('dashboard.produktivitas'));
});
document.querySelectorAll('.chart-group-panel').forEach(function (panel) {
const button = document.querySelector('[aria-controls="' + panel.id + '"]');
panel.addEventListener('hidden.bs.collapse', function (event) {
if (event.target !== panel || !button) return;
button.textContent = 'Tampilkan Chart';
document.querySelectorAll('.chart-group-header-toggle').forEach(function (header) {
header.addEventListener('keydown', function (event) {
if (event.target !== header || !['Enter', ' '].includes(event.key)) return;
event.preventDefault();
header.click();
});
panel.addEventListener('shown.bs.collapse', function (event) {
if (event.target !== panel || !button) return;
button.textContent = 'Sembunyikan Chart';
window.dispatchEvent(new Event('resize'));
});
document.querySelectorAll('.chart-group-panel').forEach(function (panel) {
const control = document.querySelector('.chart-group-header-toggle[aria-controls="' + panel.id + '"]');
if (!control) return;
const icon = control.querySelector('.chart-group-icon-toggle i');
const syncState = function () {
const expanded = panel.classList.contains('show');
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
control.setAttribute('aria-expanded', String(expanded));
control.setAttribute('aria-label', label);
control.title = label;
icon.classList.toggle('tabler-chevron-up', expanded);
icon.classList.toggle('tabler-chevron-down', !expanded);
};
syncState();
['hidden.bs.collapse', 'shown.bs.collapse'].forEach(function (eventName) {
panel.addEventListener(eventName, function (event) {
if (event.target !== panel) return;
syncState();
if (eventName === 'shown.bs.collapse') window.dispatchEvent(new Event('resize'));
});
});
});
});
+28 -25
View File
@@ -144,10 +144,10 @@
<div class="absolute bottom-3 left-3 bg-white/95 p-3 rounded-lg text-xs border border-gray-200 shadow-sm backdrop-blur-sm">
<div class="font-bold text-gray-700 mb-2">Legenda Tanaman:</div>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#6F4E37] rounded-full shadow-sm"></div> Kopi</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#eab308] rounded-full shadow-sm"></div> Vanili</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#D2691E] rounded-full shadow-sm"></div> Kakao</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9ca3af] rounded-full shadow-sm"></div> Kemiri</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#6FD68F] rounded-full shadow-sm"></div> Kopi</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#FFB46F] rounded-full shadow-sm"></div> Vanili</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9D8EF0] rounded-full shadow-sm"></div> Kakao</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9EA6B0] rounded-full shadow-sm"></div> Kemiri</div>
</div>
</div>
</div>
@@ -253,6 +253,9 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -263,19 +266,19 @@ document.addEventListener('DOMContentLoaded', function() {
const spatialData = [
{
id: 'L-001', owner: 'Yosef Neta', familySize: 5, area: '1.2 Ha', village: 'Radabata',
polygon: "15,25 75,15 90,85 20,70", fill: "rgba(34, 197, 94, 0.15)", stroke: "#16a34a",
polygon: "15,25 75,15 90,85 20,70", fill: "rgba(111, 214, 143, 0.24)", stroke: "#6FD68F",
plants: [
{ id: 'P-01', type: 'Kopi', cx: 35, cy: 35, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemupukan Organik (Okt 2023)', panen: '45 kg/musim', color: '#6F4E37' },
{ id: 'P-02', type: 'Vanili', cx: 65, cy: 45, planted: 'Jan 2022', status: 'Berbunga', intervensi: 'Pelatihan Polinasi (Feb 2024)', panen: 'Belum panen', color: '#eab308' },
{ id: 'P-03', type: 'Kopi', cx: 50, cy: 65, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemangkasan (Nov 2023)', panen: '40 kg/musim', color: '#6F4E37' },
{ id: 'P-01', type: 'Kopi', cx: 35, cy: 35, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemupukan Organik (Okt 2023)', panen: '45 kg/musim', color: pastelCategory.coffee || '#6FD68F' },
{ id: 'P-02', type: 'Vanili', cx: 65, cy: 45, planted: 'Jan 2022', status: 'Berbunga', intervensi: 'Pelatihan Polinasi (Feb 2024)', panen: 'Belum panen', color: pastelCategory.vanilla || '#FFB46F' },
{ id: 'P-03', type: 'Kopi', cx: 50, cy: 65, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemangkasan (Nov 2023)', panen: '40 kg/musim', color: pastelCategory.coffee || '#6FD68F' },
]
},
{
id: 'L-002', owner: 'Maria Goreti', familySize: 3, area: '0.8 Ha', village: 'Ubedolumolo',
polygon: "110,40 170,15 190,65 125,85", fill: "rgba(59, 130, 246, 0.15)", stroke: "#2563eb",
polygon: "110,40 170,15 190,65 125,85", fill: "rgba(95, 199, 214, 0.20)", stroke: "#5FC7D6",
plants: [
{ id: 'P-04', type: 'Kemiri', cx: 140, cy: 45, planted: 'Agu 2018', status: 'Produktif', intervensi: 'Pembersihan Gulma', panen: '120 kg/musim', color: '#9ca3af' },
{ id: 'P-05', type: 'Kakao', cx: 165, cy: 55, planted: 'Okt 2020', status: 'Terserang Hama', intervensi: 'Penyemprotan Bio-pestisida (Jan 2024)', panen: '20 kg/musim', color: '#D2691E' },
{ id: 'P-04', type: 'Kemiri', cx: 140, cy: 45, planted: 'Agu 2018', status: 'Produktif', intervensi: 'Pembersihan Gulma', panen: '120 kg/musim', color: pastelCategory.candlenut || '#9EA6B0' },
{ id: 'P-05', type: 'Kakao', cx: 165, cy: 55, planted: 'Okt 2020', status: 'Terserang Hama', intervensi: 'Penyemprotan Bio-pestisida (Jan 2024)', panen: '20 kg/musim', color: pastelCategory.cacao || '#9D8EF0' },
]
}
];
@@ -440,24 +443,24 @@ document.addEventListener('DOMContentLoaded', function() {
{
label: 'Kopi',
data: [0.5, 0.7, 1.1, 1.5],
borderColor: '#6F4E37',
backgroundColor: '#6F4E37',
borderColor: pastelCategory.coffee || '#6FD68F',
backgroundColor: pastelCategory.coffee || '#6FD68F',
tension: 0.3,
borderWidth: 2
},
{
label: 'Vanili',
data: [0.1, 0.15, 0.3, 0.5],
borderColor: '#eab308',
backgroundColor: '#eab308',
borderColor: pastelCategory.vanilla || '#FFB46F',
backgroundColor: pastelCategory.vanilla || '#FFB46F',
tension: 0.3,
borderWidth: 2
},
{
label: 'Kakao',
data: [0.4, 0.5, 0.8, 1.2],
borderColor: '#D2691E',
backgroundColor: '#D2691E',
borderColor: pastelCategory.cacao || '#9D8EF0',
backgroundColor: pastelCategory.cacao || '#9D8EF0',
tension: 0.3,
borderWidth: 2
}
@@ -467,7 +470,7 @@ document.addEventListener('DOMContentLoaded', function() {
responsive: true, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: { title: { display: true, text: 'Ton per Hektar (Ton/Ha)' }, grid: { color: '#f3f4f6' } },
y: { title: { display: true, text: 'Ton per Hektar (Ton/Ha)' }, grid: { color: gridColor } },
x: { grid: { display: false } }
},
plugins: {
@@ -489,11 +492,11 @@ document.addEventListener('DOMContentLoaded', function() {
label: 'Jumlah Bibit Didistribusikan',
data: [15000, 5200, 8000, 7000, 10000],
backgroundColor: [
'rgba(111, 78, 55, 0.8)', // Kopi
'rgba(234, 179, 8, 0.8)', // Vanili
'rgba(210, 105, 30, 0.8)', // Kakao
'rgba(156, 163, 175, 0.8)', // Kemiri
'rgba(34, 197, 94, 0.8)' // Pelindung (Hijau)
'rgba(111, 214, 143, 0.86)', // Kopi
'rgba(255, 180, 111, 0.86)', // Vanili
'rgba(157, 142, 240, 0.86)', // Kakao
'rgba(179, 181, 188, 0.86)', // Kemiri
'rgba(101, 216, 189, 0.86)' // Pelindung
],
borderRadius: 4
}
@@ -502,7 +505,7 @@ document.addEventListener('DOMContentLoaded', function() {
options: {
responsive: true, maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, grid: { color: '#f3f4f6' } },
y: { beginAtZero: true, grid: { color: gridColor } },
x: { grid: { display: false } }
},
plugins: {
@@ -515,4 +518,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
</script>
@endpush
@endpush
+26 -17
View File
@@ -219,6 +219,10 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// Inisialisasi Icons (Khusus Mockup, Vuexy mungkin pakai library icon sendiri seperti Tabler/FontAwesome)
if(typeof lucide !== 'undefined') {
@@ -233,23 +237,23 @@
data: {
labels: ['2021', '2022', '2023', '2024'],
datasets: [
{
label: 'Baseline (Tanpa Intervensi)',
data: [12, 12.5, 13, 13.5],
borderColor: '#9ca3af',
backgroundColor: 'transparent',
{
label: 'Baseline (Tanpa Intervensi)',
data: [12, 12.5, 13, 13.5],
borderColor: pastelSemantic.gray || '#9EA6B0',
backgroundColor: 'transparent',
borderDash: [5, 5],
fill: false,
tension: 0.4
fill: false,
tension: 0.4
},
{
label: 'Pendapatan Aktual',
data: [12, 15, 22, 31],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.15)', // Light green fill
fill: true,
{
label: 'Pendapatan Aktual',
data: [12, 15, 22, 31],
borderColor: pastelCategory.marketGreen || '#6FD68F',
backgroundColor: 'rgba(111, 214, 143, 0.20)',
fill: true,
tension: 0.4,
pointBackgroundColor: '#10b981',
pointBackgroundColor: pastelCategory.marketGreen || '#6FD68F',
pointBorderWidth: 2,
pointRadius: 4
}
@@ -265,7 +269,7 @@
scales: {
y: {
beginAtZero: false,
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: { callback: function(val) { return 'Rp ' + val + 'M'; } }
},
x: {
@@ -296,7 +300,12 @@
labels: ['Kopi', 'Vanili', 'Kakao', 'Kemiri'],
datasets: [{
data: [45, 25, 15, 15],
backgroundColor: ['#6F4E37', '#eab308', '#D2691E', '#9ca3af'],
backgroundColor: [
pastelCategory.coffee || '#6FD68F',
pastelCategory.vanilla || '#FFB46F',
pastelCategory.cacao || '#9D8EF0',
pastelCategory.candlenut || '#9EA6B0'
],
borderWidth: 2,
borderColor: '#ffffff',
hoverOffset: 4
@@ -324,4 +333,4 @@
}
});
</script>
@endpush
@endpush
@@ -12,9 +12,9 @@
box-shadow: 0 .25rem 1rem rgba(34, 48, 62, .08);
}
.tk-stat.green { border-left-color: #28a745; }
.tk-stat.blue { border-left-color: #007bff; }
.tk-stat.amber { border-left-color: #fd7e14; }
.tk-stat.green { border-left-color: #6FD68F; }
.tk-stat.blue { border-left-color: #5FC7D6; }
.tk-stat.amber { border-left-color: #FFB46F; }
.tk-stat-lbl {
font-size: 10px;
@@ -444,10 +444,10 @@
@forelse ($progressPetani as $petani)
@php
$barColor = match (true) {
$petani['percent'] === 100 => '#28a745',
$petani['percent'] >= 50 => '#007bff',
$petani['percent'] >= 20 => '#fd7e14',
default => '#dc3545',
$petani['percent'] === 100 => '#6FD68F',
$petani['percent'] >= 50 => '#5FC7D6',
$petani['percent'] >= 20 => '#FFB46F',
default => '#F47C86',
};
$initial = collect(explode(' ', trim($petani['name'])))
@@ -465,8 +465,8 @@
style="
width: 28px;
height: 28px;
background: rgba(40,167,69,.15);
color: #28a745;
background: rgba(111,214,143,.24);
color: #6FD68F;
"
>
{{ $initial ?: 'PT' }}
@@ -1054,12 +1054,13 @@ document.addEventListener('DOMContentLoaded', function () {
|--------------------------------------------------------------------------
*/
const primaryColor = config.colors.primary;
const secondaryColor = config.colors.secondary;
const successColor = config.colors.success;
const infoColor = config.colors.info;
const warningColor = config.colors.warning;
const dangerColor = config.colors.danger;
const pastelSemantic = config.chartColors?.semantic || {};
const primaryColor = pastelSemantic.primary || '#9D8EF0';
const secondaryColor = pastelSemantic.secondary || '#9EA6B0';
const successColor = pastelSemantic.success || '#6FD68F';
const infoColor = pastelSemantic.info || '#5FC7D6';
const warningColor = pastelSemantic.warning || '#FFB46F';
const dangerColor = pastelSemantic.danger || '#F47C86';
const bodyColor = config.colors.bodyColor;
const borderColor = config.colors.borderColor;
@@ -1346,4 +1347,4 @@ document.addEventListener('DOMContentLoaded', function () {
});
</script>
@endpush
@endpush