Merge branch 'main' of https://git.probindo.com/Probindo/database-pertani-web
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AlphaVantageMarket;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Http\Client\Pool;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Throwable;
|
||||
|
||||
class InternationalMarketController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
abort_unless(Gate::allows('super_admin') || Gate::allows('admin') || Gate::allows('fasilitator'), 403);
|
||||
|
||||
// File cache keeps external quotes independent of the application database.
|
||||
$cache = Cache::store('file');
|
||||
$marketData = $cache->remember('market.yahoo.quotes.v3', 300, function () use ($cache) {
|
||||
$commodities = [
|
||||
['name' => 'Kopi Arabica', 'symbol' => 'KC=F'],
|
||||
['name' => 'Kakao (Cocoa)', 'symbol' => 'CC=F'],
|
||||
];
|
||||
|
||||
try {
|
||||
$responses = Http::pool(function (Pool $pool) use ($commodities) {
|
||||
return array_map(fn ($commodity) => $pool->as($commodity['symbol'])
|
||||
->acceptJson()->withHeaders(['User-Agent' => 'Mozilla/5.0'])
|
||||
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
|
||||
->connectTimeout(3)->timeout(8)
|
||||
->get('https://query1.finance.yahoo.com/v8/finance/chart/'.rawurlencode($commodity['symbol']), [
|
||||
'interval' => '1d', 'range' => '5d',
|
||||
]), [...$commodities, ['symbol' => 'IDR=X']]);
|
||||
});
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$responses = [];
|
||||
}
|
||||
|
||||
$exchangeRate = $cache->get('market.yahoo.usd_idr');
|
||||
if (is_array($exchangeRate)) {
|
||||
$exchangeRate['stale'] = true;
|
||||
}
|
||||
$rateResponse = $responses['IDR=X'] ?? null;
|
||||
if ($rateResponse instanceof \Illuminate\Http\Client\Response && $rateResponse->successful()) {
|
||||
$meta = $rateResponse->json('chart.result.0.meta');
|
||||
if (is_array($meta) && ($meta['symbol'] ?? null) === 'IDR=X'
|
||||
&& ($meta['currency'] ?? null) === 'IDR'
|
||||
&& is_numeric($meta['regularMarketPrice'] ?? null) && $meta['regularMarketPrice'] > 0
|
||||
&& is_numeric($meta['regularMarketTime'] ?? null)) {
|
||||
$exchangeRate = [
|
||||
'value' => (float) $meta['regularMarketPrice'],
|
||||
'quoted_at' => gmdate('c', (int) $meta['regularMarketTime']),
|
||||
'source' => 'Yahoo Finance',
|
||||
'source_url' => 'https://finance.yahoo.com/quote/IDR%3DX/',
|
||||
'stale' => false,
|
||||
];
|
||||
$cache->put('market.yahoo.usd_idr', $exchangeRate, now()->addDays(7));
|
||||
}
|
||||
}
|
||||
|
||||
$quotes = array_map(function ($commodity) use ($responses, $cache) {
|
||||
$symbol = $commodity['symbol'];
|
||||
$key = 'market.yahoo.last.'.rawurlencode($symbol);
|
||||
$quote = $commodity + [
|
||||
'source' => 'Yahoo Finance',
|
||||
'source_url' => 'https://finance.yahoo.com/quote/'.rawurlencode($symbol).'/',
|
||||
'price' => null, 'currency' => null, 'unit' => null,
|
||||
'quoted_at' => null, 'stale' => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $responses[$symbol] ?? null;
|
||||
if (!$response instanceof \Illuminate\Http\Client\Response || !$response->successful()) {
|
||||
throw new \RuntimeException('Quote unavailable');
|
||||
}
|
||||
$meta = $response->json('chart.result.0.meta');
|
||||
if ($response->json('chart.error') || !is_array($meta)
|
||||
|| ($meta['symbol'] ?? null) !== $symbol
|
||||
|| !is_numeric($meta['regularMarketPrice'] ?? null)
|
||||
|| (float) $meta['regularMarketPrice'] <= 0
|
||||
|| !is_numeric($meta['regularMarketTime'] ?? null)
|
||||
|| !is_string($meta['currency'] ?? null)) {
|
||||
throw new \RuntimeException('Invalid quote');
|
||||
}
|
||||
|
||||
$quote['price'] = (float) $meta['regularMarketPrice'];
|
||||
$quote['currency'] = $meta['currency'];
|
||||
// Yahoo quotes Arabica in US cents/lb; cocoa and robusta in USD/tonne.
|
||||
// Apply the unit only when the API currency matches that convention.
|
||||
$quote['unit'] = match (true) {
|
||||
$symbol === 'KC=F' && $meta['currency'] === 'USX' => 'sen AS / lb',
|
||||
$symbol === 'CC=F' && $meta['currency'] === 'USD' => 'USD / ton metrik',
|
||||
default => $meta['currency'].' (satuan mengikuti sumber)',
|
||||
};
|
||||
$quote['quoted_at'] = gmdate('c', (int) $meta['regularMarketTime']);
|
||||
$cache->put($key, $quote, now()->addDays(7));
|
||||
|
||||
return $quote;
|
||||
} catch (Throwable $exception) {
|
||||
$previous = $cache->get($key);
|
||||
if (is_array($previous)) {
|
||||
$previous['stale'] = true;
|
||||
return $previous;
|
||||
}
|
||||
return $quote;
|
||||
}
|
||||
}, $commodities);
|
||||
|
||||
return ['quotes' => $quotes, 'exchange_rate' => $exchangeRate];
|
||||
});
|
||||
|
||||
$quotes = $marketData['quotes'];
|
||||
$exchangeRate = $marketData['exchange_rate'];
|
||||
foreach ($quotes as &$quote) {
|
||||
$quote['id'] = 'yahoo:'.$quote['symbol'];
|
||||
$quote['description'] = 'Estimasi konversi harga kontrak berjangka internasional.';
|
||||
$quote['period'] = $quote['quoted_at'];
|
||||
$quote['frequency'] = 'market';
|
||||
}
|
||||
unset($quote);
|
||||
$quotes = array_merge($quotes, $this->imfQuotes($cache), app(AlphaVantageMarket::class)->quotes($cache));
|
||||
$quotes = array_values(array_filter($quotes, fn ($quote) => is_numeric($quote['price']) && is_finite((float) $quote['price']) && $quote['price'] > 0));
|
||||
foreach ($quotes as &$quote) {
|
||||
// All active providers quote either US cents/lb or USD/metric tonne.
|
||||
$usdPerKg = match ($quote['unit']) {
|
||||
'sen AS / lb' => $quote['price'] / 100 / 0.45359237,
|
||||
'USD / ton metrik' => $quote['price'] / 1000,
|
||||
default => null,
|
||||
};
|
||||
$quote['price_idr_per_kg'] = $exchangeRate && $usdPerKg !== null
|
||||
? round($usdPerKg * $exchangeRate['value'], 2) : null;
|
||||
unset($quote['usd_per_kg']); // Old Alpha cache entries may contain this field.
|
||||
}
|
||||
unset($quote);
|
||||
|
||||
return response()->json([
|
||||
'quotes' => $quotes,
|
||||
'exchange_rate' => $exchangeRate,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
private function imfQuotes(Repository $cache): array
|
||||
{
|
||||
// IMF observations are monthly averages; fetch at most once a day.
|
||||
return $cache->remember('market.imf.monthly.v1', 86400, function () use ($cache) {
|
||||
try {
|
||||
$response = Http::withHeaders(['Accept' => 'text/csv', 'User-Agent' => 'Mozilla/5.0'])
|
||||
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
|
||||
->connectTimeout(3)->timeout(10)
|
||||
->get('https://api.imf.org/external/sdmx/2.1/data/IMF.RES,PCPS/G001.PCOFFOTM+PCOFFROB+PCOCO.USD.M', [
|
||||
'startPeriod' => now()->subYears(2)->format('Y-m'), 'format' => 'csvfile',
|
||||
])->throw();
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
try {
|
||||
fwrite($stream, $response->body());
|
||||
rewind($stream);
|
||||
$headers = fgetcsv($stream, 0, ',', '"', '');
|
||||
if (!$headers || array_diff(['COUNTRY', 'INDICATOR', 'DATA_TRANSFORMATION', 'FREQUENCY', 'TIME_PERIOD', 'OBS_VALUE', 'SCALE'], $headers)) {
|
||||
throw new \RuntimeException('Unexpected IMF CSV format');
|
||||
}
|
||||
$latest = [];
|
||||
while (($values = fgetcsv($stream, 0, ',', '"', '')) !== false) {
|
||||
if (count($values) !== count($headers)) continue;
|
||||
$row = array_combine($headers, $values);
|
||||
if ($row['COUNTRY'] !== 'G001' || $row['DATA_TRANSFORMATION'] !== 'USD' || $row['FREQUENCY'] !== 'M'
|
||||
|| $row['SCALE'] !== '0' || !is_numeric($row['OBS_VALUE']) || $row['OBS_VALUE'] <= 0
|
||||
|| !preg_match('/^\d{4}-M(0[1-9]|1[0-2])$/', $row['TIME_PERIOD'])) continue;
|
||||
$indicator = $row['INDICATOR'];
|
||||
if (!isset($latest[$indicator]) || $row['TIME_PERIOD'] > $latest[$indicator]['TIME_PERIOD']) {
|
||||
$latest[$indicator] = $row;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
$definitions = [
|
||||
'PCOFFOTM' => ['Kopi Arabica', 'USX', 'sen AS / lb', 'Biji kopi Arabica (Other Milds). Estimasi konversi harga acuan rata-rata bulanan IMF.'],
|
||||
'PCOFFROB' => ['Kopi Robusta', 'USX', 'sen AS / lb', 'Biji kopi Robusta. Estimasi konversi harga acuan rata-rata bulanan IMF.'],
|
||||
'PCOCO' => ['Kakao (Cocoa)', 'USD', 'USD / ton metrik', 'Biji kakao. Estimasi konversi harga acuan rata-rata bulanan IMF.'],
|
||||
];
|
||||
$quotes = [];
|
||||
foreach ($definitions as $indicator => [$name, $currency, $unit, $description]) {
|
||||
if (!isset($latest[$indicator])) continue;
|
||||
$row = $latest[$indicator];
|
||||
$quotes[] = [
|
||||
'id' => 'imf:'.$indicator, 'symbol' => $indicator, 'name' => $name,
|
||||
'price' => (float) $row['OBS_VALUE'], 'currency' => $currency, 'unit' => $unit,
|
||||
'source' => 'IMF · PCPS', 'source_url' => 'https://data.imf.org/Datasets/PCPS',
|
||||
'description' => $description, 'frequency' => 'monthly',
|
||||
'period' => str_replace('-M', '-', $row['TIME_PERIOD']), 'stale' => false,
|
||||
];
|
||||
}
|
||||
if (!$quotes) throw new \RuntimeException('No valid IMF observations');
|
||||
$cache->put('market.imf.last.v1', $quotes, now()->addDays(30));
|
||||
return $quotes;
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
return array_map(function ($quote) {
|
||||
$quote['stale'] = true;
|
||||
return $quote;
|
||||
}, $cache->get('market.imf.last.v1', []));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Throwable;
|
||||
|
||||
class AlphaVantageMarket
|
||||
{
|
||||
public function quotes(Repository $cache): array
|
||||
{
|
||||
if (!$key = config('services.alpha_vantage.api_key')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Retain existing cache keys so cleanup does not spend another API request.
|
||||
$cacheKey = 'market.additional.v1.alpha:COFFEE';
|
||||
$lastKey = 'market.additional.last.v1.alpha:COFFEE';
|
||||
if (is_array($quotes = $cache->get($cacheKey))) {
|
||||
return $quotes;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::acceptJson()
|
||||
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
|
||||
->connectTimeout(3)->timeout(6)
|
||||
->get('https://www.alphavantage.co/query', [
|
||||
'function' => 'COFFEE', 'interval' => 'monthly', 'apikey' => $key,
|
||||
])->throw();
|
||||
$data = $response->json();
|
||||
if (!is_array($data) || ($data['interval'] ?? null) !== 'monthly'
|
||||
|| strtolower($data['unit'] ?? '') !== 'cents per pound' || !is_array($data['data'] ?? null)) {
|
||||
throw new \RuntimeException('Invalid Alpha Vantage response');
|
||||
}
|
||||
$latest = null;
|
||||
foreach ($data['data'] as $row) {
|
||||
if (!is_array($row) || !is_numeric($row['value'] ?? null) || !is_finite((float) $row['value']) || $row['value'] <= 0
|
||||
|| !is_string($row['date'] ?? null) || !preg_match('/^\d{4}-(0[1-9]|1[0-2])-01$/', $row['date'])) {
|
||||
continue;
|
||||
}
|
||||
if (!$latest || $row['date'] > $latest['date']) $latest = $row;
|
||||
}
|
||||
if (!$latest) throw new \RuntimeException('No valid Alpha Vantage price');
|
||||
$quote = [
|
||||
'id' => 'alpha:COFFEE', 'name' => 'Kopi Arabica',
|
||||
'source' => 'Alpha Vantage (data IMF via FRED)',
|
||||
'source_url' => 'https://www.alphavantage.co/documentation/#coffee',
|
||||
'price' => (float) $latest['value'], 'currency' => 'USX', 'unit' => 'sen AS / lb',
|
||||
'period' => substr($latest['date'], 0, 7), 'frequency' => 'monthly', 'stale' => false,
|
||||
'description' => 'Biji kopi Arabica (Other Milds). Estimasi konversi harga acuan rata-rata bulanan IMF melalui Alpha Vantage.',
|
||||
'update_info' => 'Data Alpha Vantage diperiksa setiap hari untuk menjaga kuota API. Harga merupakan rata-rata bulanan dari IMF via FRED.',
|
||||
];
|
||||
$cache->put($lastKey, $quote, now()->addDays(30));
|
||||
$cache->put($cacheKey, [$quote], 86400);
|
||||
return [$quote];
|
||||
} catch (Throwable) {
|
||||
// Request exceptions can contain the API key in their URL; do not log them.
|
||||
$quotes = [];
|
||||
if (is_array($last = $cache->get($lastKey))) {
|
||||
$last['stale'] = true;
|
||||
$quotes[] = $last;
|
||||
}
|
||||
$cache->put($cacheKey, $quotes, 3600);
|
||||
return $quotes;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user