diff --git a/app/Http/Controllers/Web/Dashboard/SummaryController.php b/app/Http/Controllers/Web/Dashboard/SummaryController.php index 437d0d6..122d280 100644 --- a/app/Http/Controllers/Web/Dashboard/SummaryController.php +++ b/app/Http/Controllers/Web/Dashboard/SummaryController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers\Web\Dashboard; use App\Enums\AksesPasar\PraktikFermentasi; +use App\Enums\AksesPasar\SkorKesadaranPremium; +use App\Enums\AksesPasar\SkorPengalaman; use App\Enums\KeparahanSerangan; use App\Enums\KondisiTanaman; use App\Enums\PenyakitTanaman; @@ -114,70 +116,196 @@ class SummaryController extends Controller 'kecamatan_id', 'desaKelurahan_id', 'komoditas_id', + 'segmen', ]), ] + $filterData + $this->marketDashboardData($request)); } private function marketDashboardData(Request $request): array { - if (! $request->filled('komoditas_id')) { - return [ - 'selectedKomoditas' => null, - 'priceCard' => null, - ]; - } - - $selectedKomoditas = Komoditas::query() - ->whereKey($request->komoditas_id) - ->first(['id', 'nama']); - - if (! $selectedKomoditas) { - return [ - 'selectedKomoditas' => null, - 'priceCard' => null, - ]; - } - - $productCaption = $this->marketProductCaption($selectedKomoditas->nama); - $productLabel = 'Buah segar hasil petik'; + $products = [ + ['name' => 'Kakao', 'caption' => 'Harga Biji Kering', 'product' => 'Biji kering/Biji Kupas', 'variant' => 'success'], + ['name' => 'Vanili', 'caption' => 'Harga Buah Segar', 'product' => 'Buah segar hasil petik', 'variant' => 'primary'], + ['name' => 'Kopi', 'caption' => 'Harga Ceri Merah', 'product' => 'Buah segar hasil petik', 'variant' => 'warning'], + ['name' => 'Kemiri', 'caption' => 'Harga Gelondong', 'product' => 'Buah segar hasil petik', 'variant' => 'info'], + ]; + $commodities = Komoditas::query() + ->whereIn('nama', array_column($products, 'name')) + ->pluck('id', 'nama'); $petaniIds = $this->filteredPetaniIdsQuery($request); - $priceQuery = DB::table('harga_jual_panen') + $prices = DB::table('harga_jual_panen') + ->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id') ->whereIn('harga_jual_panen.petani_id', $petaniIds) + ->whereIn('petani.komoditas_id', $commodities->values()) ->where('harga_jual_panen.harga_panen_terakhir', '>', 0) - ->where('harga_jual_panen.bentuk_produk', $productLabel); - - $averagePrice = (float) (clone $priceQuery)->avg('harga_jual_panen.harga_panen_terakhir'); - $totalRecords = (int) (clone $priceQuery)->count(); + ->whereIn('harga_jual_panen.bentuk_produk', array_unique(array_column($products, 'product'))) + ->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk') + ->selectRaw('AVG(harga_jual_panen.harga_panen_terakhir) as average_price, COUNT(*) as records') + ->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk') + ->get(); return [ - 'selectedKomoditas' => $selectedKomoditas, - 'priceCard' => [ - 'value' => $averagePrice > 0 - ? 'Rp '.number_format($averagePrice, 0, ',', '.') - : 'Rp 0', - 'label' => 'Harga Jual', - 'caption' => $productCaption, - 'description' => 'Harga rata-rata buah segar hasil petik di tingkat petani', - 'records' => $totalRecords, - 'unit' => '/kg', - 'variant' => 'success', - 'icon' => 'tabler-cash', - ], + 'priceCards' => collect($products)->map(function (array $product) use ($commodities, $prices) { + $commodityId = $commodities->get($product['name']); + $price = $commodityId === null ? null : $prices->first(fn ($row) => + (int) $row->komoditas_id === (int) $commodityId + && $row->bentuk_produk === $product['product'] + ); + + return [ + 'value' => 'Rp '.number_format((float) ($price?->average_price ?? 0), 0, ',', '.'), + 'label' => $product['name'].' - '.$product['caption'], + 'description' => 'Harga rata-rata panen terakhir di tingkat petani', + 'records' => (int) ($price?->records ?? 0), + 'unit' => '/kg', + 'variant' => $product['variant'], + 'icon' => 'tabler-cash', + ]; + })->all(), + 'marketCharts' => $this->marketChannelCharts($request), + 'priceTrend' => $this->marketPriceTrend($request), + 'premiumAwareness' => $this->marketPremiumAwareness($request), ]; } - private function marketProductCaption(string $komoditasName): string + private function marketPriceTrend(Request $request): array { - $normalized = str($komoditasName)->lower()->trim()->toString(); + $products = ['Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi (Rp/kg)']; + $periods = [ + 'harga_dua_tahun_lalu' => 'Dua Tahun Lalu', + 'harga_tahun_lalu' => 'Tahun Lalu', + 'harga_panen_terakhir' => 'Panen Terakhir', + ]; + $commodities = Komoditas::query() + ->when($request->filled('komoditas_id'), fn ($query) => $query->where('id', $request->komoditas_id)) + ->orderBy('nama') + ->get(['id', 'nama']); - return match (true) { - str_contains($normalized, 'kakao') => 'Harga Buah Kakao', - str_contains($normalized, 'vanili') => 'Harga Curing', - str_contains($normalized, 'kopi') => 'Harga Ceri Merah', - str_contains($normalized, 'kemiri') => 'Harga Gelondong', - default => 'Harga Buah Segar', - }; + $query = DB::table('harga_jual_panen') + ->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id') + ->whereIn('harga_jual_panen.petani_id', $this->filteredPetaniIdsQuery($request)) + ->whereIn('petani.komoditas_id', $commodities->pluck('id')) + ->whereIn('harga_jual_panen.bentuk_produk', $products) + ->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk') + ->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk'); + + foreach (array_keys($periods) as $field) { + $query->selectRaw("AVG(CASE WHEN harga_jual_panen.{$field} > 0 THEN harga_jual_panen.{$field} END) as {$field}"); + } + + $prices = $query->get()->groupBy('komoditas_id'); + + return [ + 'periods' => array_values($periods), + 'commodities' => $commodities->map(function ($commodity) use ($products, $periods, $prices) { + $rows = $prices->get($commodity->id, collect())->keyBy('bentuk_produk'); + + return [ + 'id' => $commodity->id, + 'name' => $commodity->nama, + 'series' => collect($products)->map(function ($product) use ($periods, $rows) { + $row = $rows->get($product); + + return [ + 'name' => $product === 'Terfermentasi (Rp/kg)' ? 'Terfermentasi' : $product, + 'data' => array_map(fn ($field) => isset($row->{$field}) ? round((float) $row->{$field}, 2) : null, array_keys($periods)), + ]; + })->all(), + ]; + })->all(), + ]; + } + + private function marketChannelCharts(Request $request): array + { + $rows = DB::table('akses_pasar') + ->whereNull('deleted_at') + ->whereIn('petani_id', $this->filteredPetaniIdsQuery($request)) + ->orderBy('id') + ->get(['petani_id', 'tujuan_penjualan', 'sifat_hubungan_bisnis', 'frekuensi_penjualan', 'skor_pengalaman']) + ->unique('petani_id'); + + $channels = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir', 'Lainnya']; + $relationships = [ + 'Bebas, bisa pindah pembeli kapan saja' => 'Bebas pindah pembeli', + 'Langganan tetap, tapi tidak terikat' => 'Langganan tetap', + 'Berbasis hutang, harus jual ke sana karena ada pinjaman' => 'Berbasis hutang', + 'Titip jual,dibayar setelah produk terjual' => 'Titip jual', + 'Ada komitmen volume, sudah ada kesepakatan jumlah' => 'Komitmen volume', + 'Semi-kontrak, ada kesepakatan harga tertentu' => 'Semi-kontrak', + ]; + $timing = ['Langsung setelah panen', 'Ditumpuk dulu beberapa waktu', 'Saat butuh uang mendesak']; + $experience = collect(SkorPengalaman::options()) + ->mapWithKeys(fn ($option) => [$option['value'] => $option['label']]) + ->all(); + + return [ + 'saluranPenjualan' => $this->marketResponseChart($rows, 'tujuan_penjualan', array_combine($channels, $channels), true), + 'hubunganPembeli' => $this->marketResponseChart($rows, 'sifat_hubungan_bisnis', $relationships), + 'waktuPenjualan' => $this->marketResponseChart($rows, 'frekuensi_penjualan', array_combine($timing, $timing)), + 'aksesSaluranLain' => $this->marketResponseChart($rows, 'skor_pengalaman', $experience), + ]; + } + + private function marketPremiumAwareness(Request $request): array + { + $rows = DB::table('akses_pasar') + ->whereNull('deleted_at') + ->whereIn('petani_id', $this->filteredPetaniIdsQuery($request)) + ->orderBy('id') + ->get(['petani_id', 'skor_kesadaran_premium']) + ->unique('petani_id'); + $options = collect(SkorKesadaranPremium::options()) + ->mapWithKeys(fn ($option) => [$option['value'] => $option['label']]) + ->all(); + $chart = $this->marketResponseChart($rows, 'skor_kesadaran_premium', $options); + $chart['fullLabels'] = [ + 'Belum pernah mendengar sama sekali', + 'Pernah mendengar tapi belum paham persyaratannya', + 'Pernah mendengar dan sedikit memahami persyaratannya', + 'Sudah sangat memahami persyaratannya, tapi belum pernah mengakses pasar premium', + 'Sudah mengakses dan mendapat harga premium secara konsisten', + ]; + + return $chart; + } + + private function marketResponseChart($rows, string $field, array $options, bool $multiple = false): array + { + $counts = array_fill_keys(array_keys($options), 0); + $respondents = 0; + + foreach ($rows as $row) { + $values = $multiple ? $this->arrayValues($row->{$field}) : [$row->{$field}]; + $answers = []; + + foreach ($values as $value) { + if (is_string($value) || is_int($value)) { + $value = is_string($value) ? trim($value) : $value; + if (array_key_exists($value, $options)) { + $answers[$value] = true; + } + } + } + + if ($answers === []) { + continue; + } + + $respondents++; + foreach (array_keys($answers) as $answer) { + $counts[$answer]++; + } + } + + return [ + 'labels' => array_values($options), + 'fullLabels' => $field === 'skor_pengalaman' ? array_values($options) : array_keys($options), + 'series' => array_values($counts), + 'respondents' => $respondents, + 'percentages' => array_map(fn ($count) => $respondents > 0 ? round($count / $respondents * 100, 1) : 0, array_values($counts)), + ]; } private function productionDashboardData(Request $request): array diff --git a/resources/views/dashboard/pasar.blade.php b/resources/views/dashboard/pasar.blade.php index 6561b3d..ab7aaca 100644 --- a/resources/views/dashboard/pasar.blade.php +++ b/resources/views/dashboard/pasar.blade.php @@ -3,12 +3,12 @@ @push('style')