dashboard pasar

This commit is contained in:
2026-10-21 10:14:49 +07:00
parent 4ab745cfe5
commit 013cb7b728
4 changed files with 1039 additions and 162 deletions
@@ -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