8 Commits
37 changed files with 4167 additions and 1247 deletions
+6
View File
@@ -63,3 +63,9 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
# Optional CA bundle path for Yahoo Finance when PHP has no system CA configured.
YAHOO_FINANCE_CA_BUNDLE=
ALPHA_VANTAGE_API_KEY=
@@ -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; namespace App\Http\Controllers\Web\Dashboard;
use App\Enums\AksesPasar\PraktikFermentasi; use App\Enums\AksesPasar\PraktikFermentasi;
use App\Enums\AksesPasar\SkorKesadaranPremium;
use App\Enums\AksesPasar\SkorPengalaman;
use App\Enums\KeparahanSerangan; use App\Enums\KeparahanSerangan;
use App\Enums\KondisiTanaman; use App\Enums\KondisiTanaman;
use App\Enums\PenyakitTanaman; use App\Enums\PenyakitTanaman;
@@ -114,70 +116,196 @@ class SummaryController extends Controller
'kecamatan_id', 'kecamatan_id',
'desaKelurahan_id', 'desaKelurahan_id',
'komoditas_id', 'komoditas_id',
'segmen',
]), ]),
] + $filterData + $this->marketDashboardData($request)); ] + $filterData + $this->marketDashboardData($request));
} }
private function marketDashboardData(Request $request): array private function marketDashboardData(Request $request): array
{ {
if (! $request->filled('komoditas_id')) { $products = [
return [ ['name' => 'Kakao', 'caption' => 'Harga Biji Kering', 'product' => 'Biji kering/Biji Kupas', 'variant' => 'success'],
'selectedKomoditas' => null, ['name' => 'Vanili', 'caption' => 'Harga Buah Segar', 'product' => 'Buah segar hasil petik', 'variant' => 'primary'],
'priceCard' => null, ['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()
$selectedKomoditas = Komoditas::query() ->whereIn('nama', array_column($products, 'name'))
->whereKey($request->komoditas_id) ->pluck('id', 'nama');
->first(['id', 'nama']);
if (! $selectedKomoditas) {
return [
'selectedKomoditas' => null,
'priceCard' => null,
];
}
$productCaption = $this->marketProductCaption($selectedKomoditas->nama);
$productLabel = 'Buah segar hasil petik';
$petaniIds = $this->filteredPetaniIdsQuery($request); $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('harga_jual_panen.petani_id', $petaniIds)
->whereIn('petani.komoditas_id', $commodities->values())
->where('harga_jual_panen.harga_panen_terakhir', '>', 0) ->where('harga_jual_panen.harga_panen_terakhir', '>', 0)
->where('harga_jual_panen.bentuk_produk', $productLabel); ->whereIn('harga_jual_panen.bentuk_produk', array_unique(array_column($products, 'product')))
->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
$averagePrice = (float) (clone $priceQuery)->avg('harga_jual_panen.harga_panen_terakhir'); ->selectRaw('AVG(harga_jual_panen.harga_panen_terakhir) as average_price, COUNT(*) as records')
$totalRecords = (int) (clone $priceQuery)->count(); ->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
->get();
return [ return [
'selectedKomoditas' => $selectedKomoditas, 'priceCards' => collect($products)->map(function (array $product) use ($commodities, $prices) {
'priceCard' => [ $commodityId = $commodities->get($product['name']);
'value' => $averagePrice > 0 $price = $commodityId === null ? null : $prices->first(fn ($row) =>
? 'Rp '.number_format($averagePrice, 0, ',', '.') (int) $row->komoditas_id === (int) $commodityId
: 'Rp 0', && $row->bentuk_produk === $product['product']
'label' => 'Harga Jual', );
'caption' => $productCaption,
'description' => 'Harga rata-rata buah segar hasil petik di tingkat petani', return [
'records' => $totalRecords, 'value' => 'Rp '.number_format((float) ($price?->average_price ?? 0), 0, ',', '.'),
'unit' => '/kg', 'label' => $product['name'].' - '.$product['caption'],
'variant' => 'success', 'description' => 'Harga rata-rata panen terakhir di tingkat petani',
'icon' => 'tabler-cash', '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) { $query = DB::table('harga_jual_panen')
str_contains($normalized, 'kakao') => 'Harga Buah Kakao', ->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id')
str_contains($normalized, 'vanili') => 'Harga Curing', ->whereIn('harga_jual_panen.petani_id', $this->filteredPetaniIdsQuery($request))
str_contains($normalized, 'kopi') => 'Harga Ceri Merah', ->whereIn('petani.komoditas_id', $commodities->pluck('id'))
str_contains($normalized, 'kemiri') => 'Harga Gelondong', ->whereIn('harga_jual_panen.bentuk_produk', $products)
default => 'Harga Buah Segar', ->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 private function productionDashboardData(Request $request): array
@@ -24,7 +24,7 @@ class AksesPasarController extends Controller
{ {
$petani = Petani::findOrFail($petaniId); $petani = Petani::findOrFail($petaniId);
$aksesPasar = AksesPasar::where('petani_id', $petaniId)->first(); $aksesPasar = AksesPasar::where('petani_id', $petaniId)->latest()->first();
$hargaJualPanen = HargaJualPanen::where('petani_id', $petaniId)->get(); $hargaJualPanen = HargaJualPanen::where('petani_id', $petaniId)->get();
$hargaJualRows = collect([ $hargaJualRows = collect([
'Buah segar hasil petik', 'Buah segar hasil petik',
@@ -50,32 +50,40 @@ class AksesPasarController extends Controller
} }
public function store(Request $request) public function store(Request $request)
{ {
$validated = $this->validateData($request); $validated = $this->validateData($request);
DB::beginTransaction(); DB::beginTransaction();
try { try {
$aksesPasarData = $this->aksesPasarPayload($validated); $aksesPasarData = $this->aksesPasarPayload($validated);
$existingAksesPasar = AksesPasar::where('petani_id', $validated['petani_id'])->first();
if ($existingAksesPasar) {
$aksesPasarData['updated_by'] = Auth::id();
$existingAksesPasar->update($aksesPasarData);
$aksesPasar = $existingAksesPasar;
} else {
$aksesPasarData['uid'] = 'AKP-' . Str::uuid(); $aksesPasarData['uid'] = 'AKP-' . Str::uuid();
$aksesPasarData['created_by'] = Auth::id(); $aksesPasarData['created_by'] = Auth::id();
$aksesPasarData['updated_by'] = Auth::id(); $aksesPasarData['updated_by'] = Auth::id();
$aksesPasar = AksesPasar::create($aksesPasarData); $aksesPasar = AksesPasar::create($aksesPasarData);
if ($request->has('harga_jual')) {
$this->syncHargaJual($request->petani_id, $validated['harga_jual'] ?? [], Auth::id());
}
DB::commit();
return redirect()->back()->with('success', 'Data Rantai Nilai dan Akses Pasar berhasil disimpan.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()->withErrors(['error' => 'Gagal menyimpan data: ' . $e->getMessage()])->withInput();
} }
if ($request->has('harga_jual')) {
$this->syncHargaJual($request->petani_id, $validated['harga_jual'] ?? [], Auth::id());
}
DB::commit();
return redirect()->back()->with('success', 'Data Rantai Nilai dan Akses Pasar berhasil disimpan.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()->withErrors(['error' => 'Gagal menyimpan data: ' . $e->getMessage()])->withInput();
} }
}
public function update(Request $request, $petaniId, $id) public function update(Request $request, $petaniId, $id)
{ {
@@ -118,7 +126,8 @@ class AksesPasarController extends Controller
'skorPengalamanBerhentiValues' => [ 'skorPengalamanBerhentiValues' => [
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value, SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
] ],
'sumberInfoLainnyaValue' => 'Lainnya',
]; ];
} }
@@ -155,6 +164,14 @@ class AksesPasarController extends Controller
'nullable', 'string', 'max:255' 'nullable', 'string', 'max:255'
], ],
'tahu_harga_sebelumnya' => ['nullable', 'string', 'max:255'],
'sumber_info_harga' => ['nullable', 'array'],
'sumber_info_harga.*' => ['string'],
'sumber_info_harga_lainnya' => [
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('sumber_info_harga', []), true)),
'nullable', 'string', 'max:255'
],
'praktik_fermentasi' => ['nullable', 'string', Rule::in(PraktikFermentasi::values())], 'praktik_fermentasi' => ['nullable', 'string', Rule::in(PraktikFermentasi::values())],
'proses_fermentasi' => [ 'proses_fermentasi' => [
Rule::requiredIf(fn () => filled($request->input('praktik_fermentasi')) && $request->input('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value), Rule::requiredIf(fn () => filled($request->input('praktik_fermentasi')) && $request->input('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value),
@@ -216,7 +233,6 @@ class AksesPasarController extends Controller
{ {
$payload = collect($validated)->except(['harga_jual'])->all(); $payload = collect($validated)->except(['harga_jual'])->all();
// Bersihkan input teks "Lainnya" jika checkbox Lainnya tidak dipilih
if (!in_array('Lainnya', $payload['tujuan_penjualan'] ?? [], true)) { if (!in_array('Lainnya', $payload['tujuan_penjualan'] ?? [], true)) {
$payload['tujuan_penjualan_lainnya'] = null; $payload['tujuan_penjualan_lainnya'] = null;
} }
@@ -232,6 +248,10 @@ class AksesPasarController extends Controller
$payload['saluran_terdahulu_lainnya'] = null; $payload['saluran_terdahulu_lainnya'] = null;
} }
if (!in_array('Lainnya', $payload['sumber_info_harga'] ?? [], true)) {
$payload['sumber_info_harga_lainnya'] = null;
}
if (($payload['praktik_fermentasi'] ?? null) === PraktikFermentasi::BELUM_PERNAH->value) { if (($payload['praktik_fermentasi'] ?? null) === PraktikFermentasi::BELUM_PERNAH->value) {
$payload['proses_fermentasi'] = null; $payload['proses_fermentasi'] = null;
$payload['model_fermentasi_memungkinkan'] = null; $payload['model_fermentasi_memungkinkan'] = null;
@@ -274,7 +294,7 @@ class AksesPasarController extends Controller
$data['harga_jual'][$index]['harga_tahun_lalu'] = $this->currencyDigits($item['harga_tahun_lalu']); $data['harga_jual'][$index]['harga_tahun_lalu'] = $this->currencyDigits($item['harga_tahun_lalu']);
} }
if (array_key_exists('harga_dua_tahun_lalu', $item)) { if (array_key_exists('harga_dua_tahun_lalu', $item)) {
$data['harga_jual'][$index]['harga_dua_tahun_lalu'] = $this->currencyDigits($item['harga_dua_tahun_l'], 0); // fixed $data['harga_jual'][$index]['harga_dua_tahun_lalu'] = $this->currencyDigits($item['harga_dua_tahun_lalu']);
} }
} }
} }
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Web\Produktivitas;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@@ -31,21 +32,30 @@ class AsetKelompokTaniController extends Controller
} }
} }
private function redirectToSection(string $url, string $section, string $message) private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
{ {
return redirect($url . '#section-' . $section)->with([ return redirect($url . '#section-' . ($nextSection ?? $section))->with([
'success' => $message, 'success' => $message,
'success_section' => $section, 'success_section' => $section,
]); ]);
} }
private function kegiatanUrl(KelompokTani $kelompokTani): string
{
return route('kelompok-tani.edit', $kelompokTani->id) . '?section=aktivitas-fungsi';
}
private function nextAssetSection(KelompokTani $kelompokTani): string
{
return $kelompokTani->aset()->where('jenis_aset', 'Kas/simpanan kelompok')->where('status_aset', true)->exists()
? 'kas' : 'status-bantuan';
}
public function index(KelompokTani $kelompokTani) public function index(KelompokTani $kelompokTani)
{ {
$this->authorizeAccess(); $this->authorizeAccess();
$kelompokTani->load('aset'); return redirect($this->kegiatanUrl($kelompokTani) . '#section-aset-baku');
return view('produktivitas.survey-kelompok-tani.aset-keuangan', compact('kelompokTani'));
} }
public function update(Request $request, KelompokTani $kelompokTani) public function update(Request $request, KelompokTani $kelompokTani)
@@ -56,11 +66,26 @@ class AsetKelompokTaniController extends Controller
'aset' => ['required', 'array'], 'aset' => ['required', 'array'],
'aset.*.jenis_aset' => ['required', Rule::in(self::JENIS_ASET_BAKU)], 'aset.*.jenis_aset' => ['required', Rule::in(self::JENIS_ASET_BAKU)],
'aset.*.status_aset' => ['nullable', 'boolean'], 'aset.*.status_aset' => ['nullable', 'boolean'],
'aset.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'], 'aset.*.status_pengelolaan_aset' => ['required_if_accepted:aset.*.status_aset', 'nullable', 'string', 'max:100'],
'aset.*.kondisi_aset' => ['nullable', 'string', 'max:100'], 'aset.*.kondisi_aset' => ['required_if_accepted:aset.*.status_aset', 'nullable', 'string', 'max:100'],
...$this->asetLainnyaRules(),
], [
'aset.*.status_pengelolaan_aset.required_if_accepted' => 'Status Pengelolaan wajib diisi jika aset dipilih Ada.',
'aset.*.kondisi_aset.required_if_accepted' => 'Kondisi wajib diisi jika aset dipilih Ada.',
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
]); ]);
foreach ($validated['aset'] as $item) { DB::transaction(function () use ($kelompokTani, $validated) {
$this->saveAsetBaku($kelompokTani, $validated['aset']);
$this->createAsetLainnya($kelompokTani, $validated['aset_lainnya'] ?? []);
});
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-baku', 'Data Operasional berhasil disimpan.', $this->nextAssetSection($kelompokTani));
}
private function saveAsetBaku(KelompokTani $kelompokTani, array $items): void
{
foreach ($items as $item) {
$jenisAset = $item['jenis_aset']; $jenisAset = $item['jenis_aset'];
$ada = (bool) ($item['status_aset'] ?? false); $ada = (bool) ($item['status_aset'] ?? false);
@@ -82,8 +107,8 @@ class AsetKelompokTaniController extends Controller
$existing->update([ $existing->update([
'status_aset' => true, 'status_aset' => true,
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? $existing->status_pengelolaan_aset, 'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
'kondisi_aset' => $item['kondisi_aset'] ?? $existing->kondisi_aset, 'kondisi_aset' => $item['kondisi_aset'],
'deleted_by' => null, 'deleted_by' => null,
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
@@ -94,30 +119,40 @@ class AsetKelompokTaniController extends Controller
'uid' => 'AKT-' . strtoupper(Str::random(10)), 'uid' => 'AKT-' . strtoupper(Str::random(10)),
'jenis_aset' => $jenisAset, 'jenis_aset' => $jenisAset,
'status_aset' => true, 'status_aset' => true,
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? '-', 'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
'kondisi_aset' => $item['kondisi_aset'] ?? '-', 'kondisi_aset' => $item['kondisi_aset'],
'created_by' => auth()->id(), 'created_by' => auth()->id(),
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
} }
}
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-baku', 'Data aset produktif berhasil disimpan.'); private function asetLainnyaRules(): array
{
return [
'aset_lainnya' => ['nullable', 'array'],
'aset_lainnya.*.jenis_aset' => ['nullable', 'string', 'max:255', Rule::notIn(self::JENIS_ASET_BAKU)],
'aset_lainnya.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
'aset_lainnya.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
];
} }
public function store(Request $request, KelompokTani $kelompokTani) public function store(Request $request, KelompokTani $kelompokTani)
{ {
$this->authorizeAccess(); $this->authorizeAccess();
$validated = $request->validate([ $validated = $request->validate($this->asetLainnyaRules(), [
'aset_lainnya' => ['nullable', 'array'],
'aset_lainnya.*.jenis_aset' => ['nullable', 'string', 'max:255', Rule::notIn(self::JENIS_ASET_BAKU)],
'aset_lainnya.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
'aset_lainnya.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
], [
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.', 'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
]); ]);
foreach ($validated['aset_lainnya'] ?? [] as $item) { DB::transaction(fn () => $this->createAsetLainnya($kelompokTani, $validated['aset_lainnya'] ?? []));
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil ditambahkan.', $this->nextAssetSection($kelompokTani));
}
private function createAsetLainnya(KelompokTani $kelompokTani, array $items): void
{
foreach ($items as $item) {
$jenisAset = trim($item['jenis_aset'] ?? ''); $jenisAset = trim($item['jenis_aset'] ?? '');
if ($jenisAset === '') { if ($jenisAset === '') {
@@ -134,8 +169,6 @@ class AsetKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
} }
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-lainnya', 'Aset lainnya berhasil ditambahkan.');
} }
public function destroy(KelompokTani $kelompokTani, AsetKelompokTani $aset) public function destroy(KelompokTani $kelompokTani, AsetKelompokTani $aset)
@@ -143,11 +176,11 @@ class AsetKelompokTaniController extends Controller
$this->authorizeAccess(); $this->authorizeAccess();
abort_unless($aset->survey_kelompok_tani_id === $kelompokTani->id, 404); abort_unless($aset->survey_kelompok_tani_id === $kelompokTani->id, 404);
abort_if(in_array($aset->jenis_aset, self::JENIS_ASET_BAKU), 422, 'Aset baku diatur lewat checklist, bukan dihapus langsung.'); abort_if(in_array($aset->jenis_aset, self::JENIS_ASET_BAKU), 422, 'Operasional: gunakan pilihan Ada/Tidak pada tabel untuk menghapus aset baku.');
$aset->update(['deleted_by' => auth()->id()]); $aset->update(['deleted_by' => auth()->id()]);
$aset->delete(); $aset->delete();
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-lainnya', 'Aset lainnya berhasil dihapus.'); return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil dihapus.');
} }
} }
@@ -5,7 +5,9 @@ namespace App\Http\Controllers\Web\Produktivitas;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Models\Pertanian\KelompokTani; use App\Models\Pertanian\KelompokTani;
use App\Models\Pertanian\BantuanKelompokTani; use App\Models\Pertanian\BantuanKelompokTani;
@@ -19,9 +21,9 @@ class BantuanKelompokTaniController extends Controller
} }
} }
private function redirectToSection(string $url, string $section, string $message) private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
{ {
return redirect($url . '#section-' . $section)->with([ return redirect($url . '#section-' . ($nextSection ?? $section))->with([
'success' => $message, 'success' => $message,
'success_section' => $section, 'success_section' => $section,
]); ]);
@@ -29,7 +31,7 @@ class BantuanKelompokTaniController extends Controller
private function redirectToSectionWithError(string $url, string $section, string $message) private function redirectToSectionWithError(string $url, string $section, string $message)
{ {
return redirect($url . '#section-' . $section)->with([ return redirect($url . '#kelompok-tani-notifications')->with([
'error' => $message, 'error' => $message,
'error_section' => $section, 'error_section' => $section,
]); ]);
@@ -39,9 +41,72 @@ class BantuanKelompokTaniController extends Controller
{ {
$this->authorizeAccess(); $this->authorizeAccess();
$kelompokTani->load('bantuan'); return redirect($this->operasionalUrl($kelompokTani) . '#section-status-bantuan');
}
return view('produktivitas.survey-kelompok-tani.bantuan-penilaian', compact('kelompokTani')); private function operasionalUrl(KelompokTani $kelompokTani): string
{
return route('kelompok-tani.edit', $kelompokTani->id) . '?section=aktivitas-fungsi';
}
private function hasRecordedAid(KelompokTani $kelompokTani): bool
{
return $kelompokTani->bantuan()->whereRaw("TRIM(nama_program) <> ''")->exists();
}
private function bantuanRules(bool $pernah = true, bool $requireFirstRow = false): array
{
$exclude = Rule::excludeIf(!$pernah);
return [
'bantuan' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'array'],
'bantuan.*' => [$exclude, 'array'],
'bantuan.*.nama_program' => [$exclude, 'required_with:bantuan.*.penyelenggara,bantuan.*.bentuk_bantuan,bantuan.*.tahun', 'nullable', 'string', 'max:255'],
'bantuan.*.penyelenggara' => [$exclude, 'nullable', 'string', 'max:255'],
'bantuan.*.bentuk_bantuan' => [$exclude, 'nullable', 'string', 'max:255'],
'bantuan.*.tahun' => [$exclude, 'nullable', 'integer', 'min:1900', 'max:' . date('Y')],
'bantuan.0.nama_program' => [$exclude, Rule::requiredIf($requireFirstRow), 'required_with:bantuan.0.penyelenggara,bantuan.0.bentuk_bantuan,bantuan.0.tahun', 'nullable', 'string', 'max:255'],
'bantuan.0.penyelenggara' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'string', 'max:255'],
'bantuan.0.bentuk_bantuan' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'string', 'max:255'],
'bantuan.0.tahun' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'integer', 'min:1900', 'max:' . date('Y')],
];
}
private function bantuanMessages(): array
{
return [
'pernah_mendapatkan_bantuan.required' => 'Pengalaman Akses Bantuan/Program wajib diisi.',
'pernah_mendapatkan_bantuan.boolean' => 'Pilih Pernah atau Belum Pernah untuk Pengalaman Akses Bantuan/Program.',
'bantuan.required' => 'Isi minimal satu data bantuan jika memilih Pernah.',
'bantuan.0.nama_program.required' => 'Nama Program/Bantuan pada baris pertama wajib diisi.',
'bantuan.0.penyelenggara.required' => 'Penyelenggara pada baris pertama wajib diisi.',
'bantuan.0.bentuk_bantuan.required' => 'Bentuk Bantuan pada baris pertama wajib diisi.',
'bantuan.0.tahun.required' => 'Tahun pada baris pertama wajib diisi.',
'bantuan.*.nama_program.required_with' => 'Nama Program/Bantuan wajib diisi jika baris bantuan diisi.',
];
}
public function update(Request $request, KelompokTani $kelompokTani)
{
$this->authorizeAccess();
$validated = $request->validate([
'pernah_mendapatkan_bantuan' => ['required', 'boolean'],
...$this->bantuanRules(
$request->boolean('pernah_mendapatkan_bantuan'),
$request->boolean('pernah_mendapatkan_bantuan') && !$this->hasRecordedAid($kelompokTani)
),
], $this->bantuanMessages());
DB::transaction(function () use ($kelompokTani, $validated) {
$kelompokTani->update([
'pernah_mendapatkan_bantuan' => $validated['pernah_mendapatkan_bantuan'],
'updated_by' => auth()->id(),
]);
$this->createBantuan($kelompokTani, $validated['bantuan'] ?? []);
});
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil disimpan.');
} }
public function store(Request $request, KelompokTani $kelompokTani) public function store(Request $request, KelompokTani $kelompokTani)
@@ -50,37 +115,37 @@ class BantuanKelompokTaniController extends Controller
if (!$kelompokTani->pernah_mendapatkan_bantuan) { if (!$kelompokTani->pernah_mendapatkan_bantuan) {
return $this->redirectToSectionWithError( return $this->redirectToSectionWithError(
route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), $this->operasionalUrl($kelompokTani),
'status-bantuan', 'status-bantuan',
'Set status jadi "Pernah" dulu sebelum menambahkan daftar bantuan/program.' 'Operasional: pilih jawaban "Pernah" terlebih dahulu sebelum menambahkan data bantuan/program.'
); );
} }
$validated = $request->validate([ $validated = $request->validate($this->bantuanRules(true, !$this->hasRecordedAid($kelompokTani)), $this->bantuanMessages());
'bantuan' => ['nullable', 'array'],
'bantuan.*.nama_program' => ['nullable', 'string', 'max:255'],
'bantuan.*.penyelenggara' => ['nullable', 'string', 'max:255'],
'bantuan.*.bentuk_bantuan' => ['nullable', 'string', 'max:255'],
'bantuan.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
]);
foreach ($validated['bantuan'] ?? [] as $item) { DB::transaction(fn () => $this->createBantuan($kelompokTani, $validated['bantuan'] ?? []));
if (empty($item['nama_program']) && empty($item['penyelenggara']) && empty($item['bentuk_bantuan'])) {
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil ditambahkan.');
}
private function createBantuan(KelompokTani $kelompokTani, array $items): void
{
foreach ($items as $item) {
$namaProgram = trim($item['nama_program'] ?? '');
if ($namaProgram === '') {
continue; continue;
} }
$kelompokTani->bantuan()->create([ $kelompokTani->bantuan()->create([
'uid' => 'BKT-' . strtoupper(Str::random(10)), 'uid' => 'BKT-' . strtoupper(Str::random(10)),
'nama_program' => $item['nama_program'] ?? null, 'nama_program' => $namaProgram,
'penyelenggara' => $item['penyelenggara'] ?? null, 'penyelenggara' => $item['penyelenggara'] ?? '',
'bentuk_bantuan' => $item['bentuk_bantuan'] ?? null, 'bentuk_bantuan' => $item['bentuk_bantuan'] ?? '',
'tahun' => $item['tahun'] ?? null, 'tahun' => $item['tahun'] ?? null,
'created_by' => auth()->id(), 'created_by' => auth()->id(),
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
} }
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'bantuan', 'Data bantuan berhasil ditambahkan.');
} }
public function destroy(KelompokTani $kelompokTani, BantuanKelompokTani $bantuan) public function destroy(KelompokTani $kelompokTani, BantuanKelompokTani $bantuan)
@@ -89,9 +154,18 @@ class BantuanKelompokTaniController extends Controller
abort_unless($bantuan->survey_kelompok_tani_id === $kelompokTani->id, 404); abort_unless($bantuan->survey_kelompok_tani_id === $kelompokTani->id, 404);
if ($kelompokTani->pernah_mendapatkan_bantuan && !$kelompokTani->bantuan()
->where('id', '!=', $bantuan->id)->whereRaw("TRIM(nama_program) <> ''")->exists()) {
return $this->redirectToSectionWithError(
$this->operasionalUrl($kelompokTani),
'status-bantuan',
'Operasional: minimal satu data bantuan harus tersedia saat jawaban masih Pernah.'
);
}
$bantuan->update(['deleted_by' => auth()->id()]); $bantuan->update(['deleted_by' => auth()->id()]);
$bantuan->delete(); $bantuan->delete();
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'bantuan', 'Data bantuan berhasil dihapus.'); return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil dihapus.');
} }
} }
@@ -31,9 +31,7 @@ class PengurusKelompokTaniController extends Controller
{ {
$this->authorizeAccess(); $this->authorizeAccess();
$kelompokTani->load('pengurus'); return redirect(route('kelompok-tani.edit', $kelompokTani->id) . '#section-ringkasan');
return view('produktivitas.survey-kelompok-tani.pengurus-anggota', compact('kelompokTani'));
} }
public function store(Request $request, KelompokTani $kelompokTani) public function store(Request $request, KelompokTani $kelompokTani)
@@ -64,7 +62,7 @@ class PengurusKelompokTaniController extends Controller
]); ]);
} }
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'pengurus', 'Data pengurus berhasil ditambahkan.'); return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil ditambahkan.');
} }
public function destroy(KelompokTani $kelompokTani, PengurusKelompokTani $pengurus) public function destroy(KelompokTani $kelompokTani, PengurusKelompokTani $pengurus)
@@ -76,6 +74,6 @@ class PengurusKelompokTaniController extends Controller
$pengurus->update(['deleted_by' => auth()->id()]); $pengurus->update(['deleted_by' => auth()->id()]);
$pengurus->delete(); $pengurus->delete();
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'pengurus', 'Data pengurus berhasil dihapus.'); return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil dihapus.');
} }
} }
@@ -6,9 +6,13 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Models\Pertanian\KelompokTani; use App\Models\Pertanian\KelompokTani;
use App\Models\Wilayah\Provinsi; use App\Models\Wilayah\Provinsi;
use App\Models\Wilayah\KabupatenKota;
use App\Models\Wilayah\Kecamatan;
use App\Models\Wilayah\DesaKelurahan;
use App\Models\Pertanian\Komoditas; use App\Models\Pertanian\Komoditas;
class SurveyKelompokTaniController extends Controller class SurveyKelompokTaniController extends Controller
@@ -20,9 +24,9 @@ class SurveyKelompokTaniController extends Controller
} }
} }
private function redirectToSection(string $url, string $section, string $message) private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
{ {
return redirect($url . '#section-' . $section)->with([ return redirect($url . '#section-' . ($nextSection ?? $section))->with([
'success' => $message, 'success' => $message,
'success_section' => $section, 'success_section' => $section,
]); ]);
@@ -30,13 +34,90 @@ class SurveyKelompokTaniController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$kelompokTani = KelompokTani::query() $validated = $request->validate([
->when($request->search, fn ($query, $search) => $query->where('nama_kelompok', 'like', "%{$search}%")) 'search' => ['nullable', 'string', 'max:255'],
->latest() 'kabupaten_kota_id' => ['nullable', 'integer', 'exists:master_kabupaten_kota,id'],
->paginate($request->per_page ?? 10) 'kecamatan_id' => ['nullable', 'integer', 'exists:master_kecamatan,id'],
->appends($request->query()); 'desa_kelurahan_id' => ['nullable', 'integer', 'exists:master_desa_kelurahan,id'],
'komoditas_id' => ['nullable', 'integer', 'exists:master_komoditas,id'],
'jenis_kelompok' => ['nullable', 'string', 'max:50'],
'sort_by' => ['nullable', 'string'],
'per_page' => ['nullable', 'integer', 'in:10,25'],
]);
return view('produktivitas.survey-kelompok-tani.index', compact('kelompokTani')); $defaultKabupaten = KabupatenKota::select('id', 'nama')
->whereRaw('lower(nama) like ?', ['%ngada%'])->orderBy('nama')->first();
$allowedSorts = ['nama_asc', 'nama_desc', 'anggota_desc', 'anggota_asc'];
$filters = [
'search' => $validated['search'] ?? null,
'kabupaten_kota_id' => $validated['kabupaten_kota_id'] ?? $defaultKabupaten?->id,
'kecamatan_id' => $validated['kecamatan_id'] ?? null,
'desa_kelurahan_id' => $validated['desa_kelurahan_id'] ?? null,
'komoditas_id' => $validated['komoditas_id'] ?? null,
'jenis_kelompok' => $validated['jenis_kelompok'] ?? null,
'sort_by' => in_array($validated['sort_by'] ?? null, $allowedSorts, true)
? $validated['sort_by'] : null,
];
$query = KelompokTani::query()
->when(filled($filters['search']), function ($query) use ($filters) {
$search = '%' . Str::lower($filters['search']) . '%';
$query->where(function ($query) use ($search) {
$query->whereRaw('lower(nama_kelompok) like ?', [$search])
->orWhereRaw('lower(nama_responden) like ?', [$search]);
});
})
->when($filters['kabupaten_kota_id'], function ($query, $id) {
$query->whereHas('desaKelurahan.kecamatan', fn ($query) => $query->where('kabupaten_kota_id', $id));
})
->when($filters['kecamatan_id'], function ($query, $id) {
$query->whereHas('desaKelurahan', fn ($query) => $query->where('kecamatan_id', $id));
})
->when($filters['desa_kelurahan_id'], fn ($query, $id) => $query->where('desa_kelurahan_id', $id))
->when($filters['komoditas_id'], function ($query, $id) {
$query->where(function ($query) use ($id) {
$query->whereJsonContains('komoditas_id', (string) $id)
->orWhereJsonContains('komoditas_id', (int) $id);
});
})
->when($filters['jenis_kelompok'], fn ($query, $jenis) => $query->where('jenis_kelompok', $jenis));
$summary = (clone $query)->selectRaw('
COUNT(*) AS total_kelompok,
COUNT(DISTINCT jenis_kelompok) AS total_jenis,
COALESCE(SUM(jumlah_pengurus), 0) AS total_pengurus,
COALESCE(SUM(jumlah_pengurus_aktif), 0) AS pengurus_aktif,
COALESCE(SUM(jumlah_anggota_terdaftar), 0) AS total_anggota,
COALESCE(SUM(jumlah_anggota_aktif), 0) AS anggota_aktif
')->first();
match ($filters['sort_by']) {
'nama_asc' => $query->orderBy('nama_kelompok'),
'nama_desc' => $query->orderByDesc('nama_kelompok'),
'anggota_desc' => $query->orderByRaw('COALESCE(jumlah_anggota_terdaftar, 0) DESC'),
'anggota_asc' => $query->orderByRaw('COALESCE(jumlah_anggota_terdaftar, 0) ASC'),
default => $query->latest(),
};
$perPage = (int) ($validated['per_page'] ?? 10);
$kelompokTani = $query->with('desaKelurahan.kecamatan')->orderByDesc('id')
->paginate($perPage)->appends([...$filters, 'per_page' => $perPage]);
$selectedKabupaten = $filters['kabupaten_kota_id']
? KabupatenKota::select('id', 'nama')->find($filters['kabupaten_kota_id']) : null;
$kecamatanOptions = $filters['kabupaten_kota_id']
? Kecamatan::select('id', 'nama')->where('kabupaten_kota_id', $filters['kabupaten_kota_id'])->orderBy('nama')->get()
: collect();
$desaOptions = $filters['kecamatan_id']
? DesaKelurahan::select('id', 'nama')->where('kecamatan_id', $filters['kecamatan_id'])->orderBy('nama')->get()
: collect();
$komoditasOptions = Komoditas::select('id', 'nama')->orderBy('nama')->get();
$jenisKelompokOptions = KelompokTani::query()->whereNotNull('jenis_kelompok')
->where('jenis_kelompok', '!=', '')->distinct()->orderBy('jenis_kelompok')->pluck('jenis_kelompok');
return view('produktivitas.survey-kelompok-tani.index', compact(
'kelompokTani', 'summary', 'filters', 'selectedKabupaten',
'kecamatanOptions', 'desaOptions', 'komoditasOptions', 'jenisKelompokOptions'
));
} }
public function create() public function create()
@@ -46,7 +127,24 @@ class SurveyKelompokTaniController extends Controller
$provinsi = Provinsi::orderBy('kode', 'asc')->get(); $provinsi = Provinsi::orderBy('kode', 'asc')->get();
$komoditas = Komoditas::where('is_utama', true)->get(); $komoditas = Komoditas::where('is_utama', true)->get();
return view('produktivitas.survey-kelompok-tani.add', compact('provinsi', 'komoditas')); return view('produktivitas.survey-kelompok-tani.add', [
...compact('provinsi', 'komoditas'),
...$this->profileLocationOptions(),
]);
}
private function profileLocationOptions(?KelompokTani $kelompokTani = null): array
{
$locationId = static fn ($value) => is_scalar($value) && filter_var($value, FILTER_VALIDATE_INT) !== false ? (int) $value : null;
$provinsiId = $locationId(old('provinsi_id', $kelompokTani?->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id));
$kabupatenId = $locationId(old('kabupaten_id', $kelompokTani?->desaKelurahan?->kecamatan?->kabupaten_kota_id));
$kecamatanId = $locationId(old('kecamatan_id', $kelompokTani?->desaKelurahan?->kecamatan_id));
return [
'kabupatenOptions' => filled($provinsiId) ? KabupatenKota::where('provinsi_id', $provinsiId)->orderBy('nama')->get() : collect(),
'kecamatanOptions' => filled($kabupatenId) ? Kecamatan::where('kabupaten_kota_id', $kabupatenId)->orderBy('nama')->get() : collect(),
'desaOptions' => filled($kecamatanId) ? DesaKelurahan::where('kecamatan_id', $kecamatanId)->orderBy('nama')->get() : collect(),
];
} }
public function store(Request $request) public function store(Request $request)
@@ -67,7 +165,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data kelompok tani berhasil ditambahkan.'); return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil ditambahkan.', 'pengurus');
} }
private function editUrl(KelompokTani $kelompokTani, string $section = 'identitas'): string private function editUrl(KelompokTani $kelompokTani, string $section = 'identitas'): string
@@ -83,12 +181,15 @@ class SurveyKelompokTaniController extends Controller
return match ($request->query('section', 'identitas')) { return match ($request->query('section', 'identitas')) {
'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani')), 'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani')),
'aktivitas-fungsi' => view('produktivitas.survey-kelompok-tani.aktivitas-fungsi', compact('kelompokTani')), 'aktivitas-fungsi' => view('produktivitas.survey-kelompok-tani.aktivitas-fungsi', [
'kelompokTani' => $kelompokTani->loadMissing(['aset', 'bantuan']),
]),
'sosial-konflik' => view('produktivitas.survey-kelompok-tani.sosial-konflik', compact('kelompokTani')), 'sosial-konflik' => view('produktivitas.survey-kelompok-tani.sosial-konflik', compact('kelompokTani')),
default => view('produktivitas.survey-kelompok-tani.edit', [ default => view('produktivitas.survey-kelompok-tani.edit', [
'kelompokTani' => $kelompokTani, 'kelompokTani' => $kelompokTani->loadMissing('pengurus'),
'provinsi' => Provinsi::orderBy('kode', 'asc')->get(), 'provinsi' => Provinsi::orderBy('kode', 'asc')->get(),
'komoditas' => Komoditas::where('is_utama', true)->get(), 'komoditas' => Komoditas::where('is_utama', true)->get(),
...$this->profileLocationOptions($kelompokTani),
]), ]),
}; };
} }
@@ -103,8 +204,8 @@ class SurveyKelompokTaniController extends Controller
'aktivitas-fungsi' => $this->updateAktivitasFungsi($request, $kelompokTani), 'aktivitas-fungsi' => $this->updateAktivitasFungsi($request, $kelompokTani),
'kas' => $this->updateKasSimpanPinjam($request, $kelompokTani), 'kas' => $this->updateKasSimpanPinjam($request, $kelompokTani),
'sosial-konflik' => $this->updateSosialKonflik($request, $kelompokTani), 'sosial-konflik' => $this->updateSosialKonflik($request, $kelompokTani),
'status-bantuan' => $this->updateStatusBantuan($request, $kelompokTani), 'status-bantuan' => app(BantuanKelompokTaniController::class)->update($request, $kelompokTani),
'penilaian' => $this->updatePenilaian($request, $kelompokTani), 'penilaian' => redirect($this->editUrl($kelompokTani, 'aktivitas-fungsi') . '#section-status-bantuan'),
default => $this->updateIdentitas($request, $kelompokTani), default => $this->updateIdentitas($request, $kelompokTani),
}; };
} }
@@ -122,7 +223,7 @@ class SurveyKelompokTaniController extends Controller
private function validateIdentitas(Request $request) private function validateIdentitas(Request $request)
{ {
return $request->validate([ $validated = $request->validate([
// Data Responden // Data Responden
'nama_responden' => ['required', 'string', 'max:255'], 'nama_responden' => ['required', 'string', 'max:255'],
'jabatan_responden' => ['nullable', 'string', 'max:255'], 'jabatan_responden' => ['nullable', 'string', 'max:255'],
@@ -130,7 +231,10 @@ class SurveyKelompokTaniController extends Controller
// Identitas Kelompok Tani // Identitas Kelompok Tani
'nama_kelompok' => ['required', 'string', 'max:255'], 'nama_kelompok' => ['required', 'string', 'max:255'],
'desa_kelurahan_id' => ['required', 'exists:master_desa_kelurahan,id'], 'provinsi_id' => ['bail', 'required', 'integer', 'exists:master_provinsi,id'],
'kabupaten_id' => ['bail', 'required', 'integer', 'exists:master_kabupaten_kota,id'],
'kecamatan_id' => ['bail', 'required', 'integer', 'exists:master_kecamatan,id'],
'desa_kelurahan_id' => ['bail', 'required', 'integer', 'exists:master_desa_kelurahan,id'],
'dusun' => ['nullable', 'string', 'max:255'], 'dusun' => ['nullable', 'string', 'max:255'],
'tahun_berdiri' => ['nullable', 'digits:4', 'integer', 'min:1900', 'max:' . date('Y')], 'tahun_berdiri' => ['nullable', 'digits:4', 'integer', 'min:1900', 'max:' . date('Y')],
'jenis_kelompok' => ['required', 'string', 'max:50'], 'jenis_kelompok' => ['required', 'string', 'max:50'],
@@ -138,15 +242,30 @@ class SurveyKelompokTaniController extends Controller
'nama_gapoktan' => ['nullable', 'string', 'max:255'], 'nama_gapoktan' => ['nullable', 'string', 'max:255'],
'komoditas_id' => ['nullable', 'array'], 'komoditas_id' => ['nullable', 'array'],
'komoditas_id.*' => ['exists:master_komoditas,id'], 'komoditas_id.*' => ['exists:master_komoditas,id'],
...$this->keanggotaanRules(),
], [ ], [
'nama_responden.required' => 'Nama responden wajib diisi.', 'nama_responden.required' => 'Nama responden wajib diisi.',
'nama_kelompok.required' => 'Nama kelompok wajib diisi.', 'nama_kelompok.required' => 'Nama kelompok wajib diisi.',
'provinsi_id.required' => 'Provinsi wajib diisi.',
'kabupaten_id.required' => 'Kabupaten/Kota wajib diisi.',
'kecamatan_id.required' => 'Kecamatan wajib diisi.',
'provinsi_id.integer' => 'Pilihan Provinsi tidak valid.',
'kabupaten_id.integer' => 'Pilihan Kabupaten/Kota tidak valid.',
'kecamatan_id.integer' => 'Pilihan Kecamatan tidak valid.',
'desa_kelurahan_id.integer' => 'Pilihan Desa/Kelurahan tidak valid.',
'provinsi_id.exists' => 'Provinsi tidak ditemukan.',
'kabupaten_id.exists' => 'Kabupaten/Kota tidak ditemukan.',
'kecamatan_id.exists' => 'Kecamatan tidak ditemukan.',
'desa_kelurahan_id.required' => 'Desa/Kelurahan wajib dipilih.', 'desa_kelurahan_id.required' => 'Desa/Kelurahan wajib dipilih.',
'desa_kelurahan_id.exists' => 'Desa/Kelurahan tidak ditemukan.', 'desa_kelurahan_id.exists' => 'Desa/Kelurahan tidak ditemukan.',
'jenis_kelompok.required' => 'Jenis kelompok wajib dipilih.', 'jenis_kelompok.required' => 'Terdaftar/Dikenal Sebagai wajib diisi.',
'jenis_kelompok_lainnya.required_if' => 'Sebutkan jenis kelompok lainnya.', 'jenis_kelompok_lainnya.required_if' => 'Sebutkan Jenis Kelompok wajib diisi.',
'tahun_berdiri.digits' => 'Tahun berdiri harus 4 digit.', 'tahun_berdiri.digits' => 'Tahun berdiri harus 4 digit.',
]); ]);
unset($validated['provinsi_id'], $validated['kabupaten_id'], $validated['kecamatan_id']);
return $validated;
} }
private function updateIdentitas(Request $request, KelompokTani $kelompokTani) private function updateIdentitas(Request $request, KelompokTani $kelompokTani)
@@ -163,12 +282,12 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data kelompok tani berhasil diperbarui.'); return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
} }
private function updateRingkasanPengurus(Request $request, KelompokTani $kelompokTani) private function keanggotaanRules(): array
{ {
$validated = $request->validate([ return [
'jumlah_pengurus' => ['nullable', 'integer', 'min:0'], 'jumlah_pengurus' => ['nullable', 'integer', 'min:0'],
'jumlah_pengurus_aktif' => ['nullable', 'integer', 'min:0'], 'jumlah_pengurus_aktif' => ['nullable', 'integer', 'min:0'],
'jumlah_anggota_terdaftar' => ['nullable', 'integer', 'min:0'], 'jumlah_anggota_terdaftar' => ['nullable', 'integer', 'min:0'],
@@ -179,18 +298,24 @@ class SurveyKelompokTaniController extends Controller
'jumlah_anggota_perempuan' => ['nullable', 'integer', 'min:0'], 'jumlah_anggota_perempuan' => ['nullable', 'integer', 'min:0'],
'jumlah_anggota_pemuda_laki_laki' => ['nullable', 'integer', 'min:0'], 'jumlah_anggota_pemuda_laki_laki' => ['nullable', 'integer', 'min:0'],
'jumlah_anggota_pemuda_perempuan' => ['nullable', 'integer', 'min:0'], 'jumlah_anggota_pemuda_perempuan' => ['nullable', 'integer', 'min:0'],
]); ];
}
private function updateRingkasanPengurus(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate($this->keanggotaanRules());
$kelompokTani->update([ $kelompokTani->update([
...$validated, ...$validated,
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'ringkasan', 'Ringkasan pengurus & anggota berhasil disimpan.'); return $this->redirectToSection($this->editUrl($kelompokTani), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
} }
private function updateKelembagaan(Request $request, KelompokTani $kelompokTani) private function updateKelembagaan(Request $request, KelompokTani $kelompokTani)
{ {
$hasSk = in_array($request->input('status_legalitas'), KelompokTani::STATUS_LEGALITAS_DENGAN_SK, true);
$validated = $request->validate([ $validated = $request->validate([
// Cakupan Wilayah Kelompok // Cakupan Wilayah Kelompok
'cakupan_wilayah' => ['nullable', 'string', 'max:50'], 'cakupan_wilayah' => ['nullable', 'string', 'max:50'],
@@ -198,17 +323,9 @@ class SurveyKelompokTaniController extends Controller
// Status Legalitas Kelompok // Status Legalitas Kelompok
'status_legalitas' => ['nullable', 'string', 'max:50'], 'status_legalitas' => ['nullable', 'string', 'max:50'],
'nomor_sk' => ['nullable', 'string', 'max:100'], 'nomor_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:100'],
'tanggal_tahun_penerbitan_sk' => ['nullable', 'string', 'max:30'], 'tanggal_tahun_penerbitan_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:30'],
// Kualitas Kepemimpinan
'tingkat_keaktifan_ketua' => ['nullable', 'string', 'max:30'],
'status_pembagian_peran' => ['nullable', 'string', 'max:30'],
'status_dokumentasi_rapat' => ['nullable', 'string', 'max:30'],
// Frekuensi Pertemuan Rutin
'frekuensi_pertemuan' => ['nullable', 'string', 'max:50'],
'tanggal_pertemuan_terakhir' => ['nullable', 'date'],
]); ]);
$kelompokTani->update([ $kelompokTani->update([
@@ -216,7 +333,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data kelembagaan berhasil disimpan.'); return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data Kelembagaan berhasil disimpan.');
} }
private function resolveLainnya(?array $selected, ?string $lainnyaText): ?array private function resolveLainnya(?array $selected, ?string $lainnyaText): ?array
@@ -233,6 +350,10 @@ class SurveyKelompokTaniController extends Controller
private function updateAktivitasFungsi(Request $request, KelompokTani $kelompokTani) private function updateAktivitasFungsi(Request $request, KelompokTani $kelompokTani)
{ {
$validated = $request->validate([ $validated = $request->validate([
// Frekuensi Pertemuan Rutin
'frekuensi_pertemuan' => ['nullable', 'string', 'max:50'],
'tanggal_pertemuan_terakhir' => ['nullable', 'date'],
// Kegiatan Konkret yang Sudah Berjalan // Kegiatan Konkret yang Sudah Berjalan
'kegiatan_terlaksana' => ['nullable', 'array'], 'kegiatan_terlaksana' => ['nullable', 'array'],
'kegiatan_terlaksana.*' => ['string', 'max:255'], 'kegiatan_terlaksana.*' => ['string', 'max:255'],
@@ -270,7 +391,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data aktivitas & fungsi kelompok berhasil disimpan.'); return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data Operasional berhasil disimpan.', 'aset-baku');
} }
private function updateKasSimpanPinjam(Request $request, KelompokTani $kelompokTani) private function updateKasSimpanPinjam(Request $request, KelompokTani $kelompokTani)
@@ -287,22 +408,28 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'kas', 'Data kas/simpan-pinjam berhasil disimpan.'); return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'kas', 'Data Operasional berhasil disimpan.', 'status-bantuan');
} }
private function updateSosialKonflik(Request $request, KelompokTani $kelompokTani) private function updateSosialKonflik(Request $request, KelompokTani $kelompokTani)
{ {
$pernahKonflik = in_array($request->input('konflik_kelompok'), [
'Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai',
], true);
$validated = $request->validate([ $validated = $request->validate([
// Keterlibatan Perempuan // Keterlibatan Perempuan
'partisipasi_perempuan' => ['nullable', 'string', 'max:255'], 'partisipasi_perempuan' => ['nullable', 'string', 'max:255'],
// Riwayat Konflik Internal Kelompok // Riwayat Konflik Internal Kelompok
'konflik_kelompok' => ['nullable', 'string', 'max:255'], 'konflik_kelompok' => ['nullable', 'string', 'max:255'],
'topik_konflik' => ['nullable', 'string', 'max:255'], 'topik_konflik' => [Rule::requiredIf($pernahKonflik), 'nullable', 'string', 'max:255'],
'penyelesaian_konflik' => ['nullable', 'string'], 'penyelesaian_konflik' => ['nullable', 'string'],
], [
'topik_konflik.required' => 'Jika Pernah, Soal Apa? wajib diisi jika pernah terjadi konflik internal.',
]); ]);
if (!in_array($validated['konflik_kelompok'] ?? null, ['Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai'])) { if (!$pernahKonflik) {
$validated['topik_konflik'] = null; $validated['topik_konflik'] = null;
$validated['penyelesaian_konflik'] = null; $validated['penyelesaian_konflik'] = null;
} }
@@ -312,35 +439,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(), 'updated_by' => auth()->id(),
]); ]);
return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data sosial & konflik berhasil disimpan.'); return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data Sosial berhasil disimpan.');
} }
private function updateStatusBantuan(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate([
'pernah_mendapatkan_bantuan' => ['required', 'boolean'],
]);
$kelompokTani->update([
...$validated,
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'status-bantuan', 'Status akses bantuan berhasil disimpan.');
}
private function updatePenilaian(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate([
'potensi_kelompok' => ['nullable', 'string', 'max:50'],
'alasan_penilaian_potensi' => ['nullable', 'string'],
]);
$kelompokTani->update([
...$validated,
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'penilaian', 'Penilaian potensi kelompok berhasil disimpan.');
}
} }
+4
View File
@@ -31,6 +31,9 @@ class AksesPasar extends Model
'saluran_terdahulu', 'saluran_terdahulu',
'saluran_terdahulu_lainnya', 'saluran_terdahulu_lainnya',
'alasan_berhenti', 'alasan_berhenti',
'tahu_harga_sebelumnya',
'sumber_info_harga',
'sumber_info_harga_lainnya',
'praktik_fermentasi', 'praktik_fermentasi',
'proses_fermentasi', 'proses_fermentasi',
'model_fermentasi_memungkinkan', 'model_fermentasi_memungkinkan',
@@ -51,6 +54,7 @@ class AksesPasar extends Model
protected $casts = [ protected $casts = [
'tujuan_penjualan' => 'array', 'tujuan_penjualan' => 'array',
'saluran_terdahulu' => 'array', 'saluran_terdahulu' => 'array',
'sumber_info_harga' => 'array',
]; ];
protected static function booted(): void protected static function booted(): void
+5
View File
@@ -14,6 +14,11 @@ class KelompokTani extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
public const STATUS_LEGALITAS_DENGAN_SK = [
'Sudah ada SK dari desa/kecamatan',
'Sudah ada SK dan terdaftar di Dinas/Kabupaten',
];
protected $table = 'survey_kelompok_tani'; protected $table = 'survey_kelompok_tani';
protected $fillable = [ protected $fillable = [
+68
View File
@@ -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;
}
}
}
+6
View File
@@ -14,6 +14,12 @@ return [
| |
*/ */
'alpha_vantage' => ['api_key' => env('ALPHA_VANTAGE_API_KEY')],
'yahoo_finance' => [
'ca_bundle' => env('YAHOO_FINANCE_CA_BUNDLE'),
],
'postmark' => [ 'postmark' => [
'key' => env('POSTMARK_API_KEY'), 'key' => env('POSTMARK_API_KEY'),
], ],
@@ -61,7 +61,7 @@ return new class extends Migration
$table->integer('harga_tahun_lalu')->nullable(); $table->integer('harga_tahun_lalu')->nullable();
$table->integer('harga_dua_tahun_lalu')->nullable(); $table->integer('harga_dua_tahun_lalu')->nullable();
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null'); $table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
$table ->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null'); $table ->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
$table ->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null'); $table ->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null');
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('akses_pasar', function (Blueprint $table) {
$table->string('tahu_harga_sebelumnya')->nullable();
$table->text('sumber_info_harga')->nullable();
$table->string('sumber_info_harga_lainnya')->nullable();
});
}
public function down(): void
{
Schema::table('akses_pasar', function (Blueprint $table) {
$table->dropColumn([
'tahu_harga_sebelumnya',
'sumber_info_harga',
'sumber_info_harga_lainnya',
]);
});
}
};
+69
View File
@@ -185,3 +185,72 @@
color: inherit; color: inherit;
} }
#layout-menu .menu-sub .sidebar-icon > .ti { width: 19px; height: 19px; font-size: 19px; } #layout-menu .menu-sub .sidebar-icon > .ti { width: 19px; height: 19px; font-size: 19px; }
/* Profil Kelompok Tani */
.kelompok-tani-page {
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
}
.kelompok-tani-page :is(.form-control, .form-select, .form-check-label, .select2-container) {
font-family: var(--bs-body-font-family);
}
.kelompok-tani-page select.is-invalid + .select2-container .select2-selection {
border-color: var(--bs-form-invalid-border-color);
}
.kelompok-tani-page .kelompok-tani-summary-name {
font-size: .9rem;
}
.kelompok-tani-page .kelompok-tani-summary-note {
font-size: .75rem;
line-height: 1.3;
}
.kelompok-tani-page .kelompok-tani-help {
margin-top: -.75rem;
}
.kelompok-tani-page #section-kas[hidden],
.kelompok-tani-page #section-bantuan[hidden] {
display: none !important;
}
.kelompok-tani-page #kelompok-tani-notifications,
.kelompok-tani-page [id^="section-"] {
scroll-margin-top: 7rem;
}
.kelompok-tani-page .select2-selection {
position: relative;
}
.kelompok-tani-page .select2-selection::after {
content: '';
position: absolute;
top: 50%;
inset-inline-end: 1rem;
width: .5rem;
height: .5rem;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
color: var(--bs-body-color);
transform: translateY(-75%) rotate(45deg);
pointer-events: none;
}
.kelompok-tani-page .select2-selection__arrow {
display: none;
}
.kelompok-tani-page .select2-container .select2-selection--multiple .select2-selection__rendered {
padding-inline-end: 2.5rem;
}
.kelompok-tani-page .select2-container--disabled .select2-selection::after {
color: var(--bs-secondary-color);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+769 -63
View File
@@ -3,12 +3,12 @@
@push('style') @push('style')
<style> <style>
.pasar-filter-card .filter-item { .pasar-filter-card .filter-item {
min-width: 210px; min-width: 190px;
} }
.pasar-kpi-value { .pasar-kpi-value {
color: var(--bs-heading-color); color: var(--bs-heading-color);
font-size: 1.85rem; font-size: 1.6rem;
font-weight: 600; font-weight: 600;
line-height: 1.2; line-height: 1.2;
overflow-wrap: anywhere; overflow-wrap: anywhere;
@@ -25,19 +25,164 @@
line-height: 1.4; line-height: 1.4;
} }
.pasar-empty-state { .chart-group-header-toggle {
min-height: 220px; 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: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;
}
.pasar-chart-card .card-title h5 {
color: var(--bs-heading-color);
font-size: 1.125rem;
font-weight: 500;
line-height: 1.35;
letter-spacing: 0;
}
.pasar-chart-card .card-title small,
.chart-group-header-toggle p {
font-size: 0.8125rem;
line-height: 1.4;
}
.pasar-chart {
min-height: 330px;
}
.pasar-chart-empty {
min-height: 330px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--bs-secondary-color);
font-size: 0.8125rem;
text-align: center; text-align: center;
} }
.pasar-chart-card .apexcharts-tooltip {
background: transparent !important;
border: none !important;
box-shadow: none !important;
overflow: visible !important;
white-space: normal;
max-width: 260px;
}
.pasar-chart-tooltip {
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, .15);
padding: 8px 12px;
font-size: 13px;
font-weight: 500;
line-height: 1.25;
}
.pasar-trend-tooltip-row {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 6px;
}
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.pasar-filter-card .filter-item { .pasar-filter-card .filter-item {
width: 100%; width: 100%;
} }
} }
.market-info-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex: 0 0 20px;
padding: 0;
border: 1px solid var(--bs-secondary-color);
border-radius: 50%;
background: transparent;
color: var(--bs-secondary-color);
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.market-info-button:hover, .market-info-button:focus-visible {
color: var(--bs-primary);
border-color: var(--bs-primary);
outline: 2px solid rgba(var(--bs-primary-rgb), .2);
outline-offset: 2px;
}
.international-provider-logo {
width: 48px;
height: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.international-provider-logo[hidden] { display: none; }
.international-provider-logo img { object-fit: contain; }
.international-market-track {
display: flex;
gap: 1.25rem;
overflow-x: auto;
scroll-snap-type: none;
scrollbar-width: none;
padding: 2px 2px 16px;
}
.international-market-track::-webkit-scrollbar { display: none; }
.international-market-card {
flex: 0 0 calc((100% - 2.5rem) / 3);
min-width: 0;
scroll-snap-align: start;
}
@media (max-width: 991.98px) {
.international-market-card { flex-basis: calc((100% - 1.25rem) / 2); }
}
@media (max-width: 575.98px) {
.international-market-card { flex-basis: 100%; }
}
</style> </style>
@endpush @endpush
@@ -47,35 +192,6 @@
<form id="form-filter" class="col-12" method="GET" action="{{ route('dashboard.pasar') }}"> <form id="form-filter" class="col-12" method="GET" action="{{ route('dashboard.pasar') }}">
<div class="card card-border-shadow-primary pasar-filter-card"> <div class="card card-border-shadow-primary pasar-filter-card">
<div class="card-body py-3"> <div class="card-body py-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center me-2">
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
<span class="fw-medium">Komoditas <span class="text-danger">*</span></span>
</div>
<div class="filter-item">
<select id="filter-komoditas" class="form-select form-select-sm" name="komoditas_id">
<option value="">Pilih Komoditas</option>
@foreach ($komoditas as $item)
<option value="{{ $item->id }}" {{ old('komoditas_id', $komoditas_id) == $item->id ? 'selected' : '' }}>
{{ $item->nama }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-outline-secondary btn-sm">
<i class="icon-base ti tabler-filter me-1"></i>
Filter
</button>
<button type="button" id="btn-reset-dashboard-filter" class="btn btn-outline-secondary btn-sm">
<i class="icon-base ti tabler-refresh me-1"></i>
Reset
</button>
</div>
<hr class="my-3">
<div class="d-flex align-items-center gap-2 flex-wrap"> <div class="d-flex align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center me-2"> <div class="d-flex align-items-center me-2">
<i class="icon-base ti tabler-map-2 me-2"></i> <i class="icon-base ti tabler-map-2 me-2"></i>
@@ -111,39 +227,57 @@
</select> </select>
</div> </div>
</div> </div>
<hr class="my-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center me-2">
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
<span class="fw-medium">Data</span>
</div>
<div class="filter-item">
<select id="filter-komoditas" class="form-select form-select-sm" name="komoditas_id">
<option value="">Semua Komoditas</option>
@foreach ($komoditas as $item)
<option value="{{ $item->id }}" {{ old('komoditas_id', $komoditas_id) == $item->id ? 'selected' : '' }}>
{{ $item->nama }}
</option>
@endforeach
</select>
</div>
<div class="filter-item">
<select id="filter-segmen" class="form-select form-select-sm" name="segmen">
<option value="">Semua Segmen</option>
@foreach ($segments as $segment)
<option value="{{ $segment['name'] }}" {{ old('segmen', $segmen) == $segment['name'] ? 'selected' : '' }}>
{{ $segment['name'] }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-outline-secondary btn-sm">
<i class="icon-base ti tabler-filter me-1"></i>
Filter
</button>
<button type="button" id="btn-reset-dashboard-filter" class="btn btn-outline-secondary btn-sm">
<i class="icon-base ti tabler-refresh me-1"></i>
Reset
</button>
</div>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
@if (! $selectedKomoditas) <div class="row g-4 mb-4">
<div class="alert alert-primary d-flex align-items-start gap-3" role="alert"> @foreach ($priceCards as $priceCard)
<i class="icon-base ti tabler-info-circle mt-1"></i> <div class="col-12 col-md-6 col-xl-3">
<div>
<h6 class="alert-heading mb-1">Pilih komoditas terlebih dahulu</h6>
<div>Data dashboard pasar baru akan ditampilkan setelah filter komoditas wajib diisi.</div>
</div>
</div>
<div class="card">
<div class="card-body pasar-empty-state">
<div>
<div class="avatar avatar-lg mx-auto mb-3">
<span class="avatar-initial rounded bg-label-primary">
<i class="icon-base ti tabler-building-store icon-28px"></i>
</span>
</div>
<h5 class="mb-1">Belum ada data pasar yang ditampilkan</h5>
<p class="text-body-secondary mb-0">Gunakan filter komoditas untuk melihat harga jual rata-rata.</p>
</div>
</div>
</div>
@else
<div class="row g-4 mb-4">
<div class="col-xl-4 col-md-6">
<div class="card card-border-shadow-{{ $priceCard['variant'] }} h-100"> <div class="card card-border-shadow-{{ $priceCard['variant'] }} h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center gap-4"> <div class="d-flex flex-column align-items-start gap-3">
<div class="avatar avatar-lg"> <div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-{{ $priceCard['variant'] }}"> <span class="avatar-initial rounded bg-label-{{ $priceCard['variant'] }}">
<i class="icon-base ti {{ $priceCard['icon'] }} icon-28px"></i> <i class="icon-base ti {{ $priceCard['icon'] }} icon-28px"></i>
@@ -161,22 +295,594 @@
</div> </div>
</div> </div>
<div class="mt-4 pt-3 border-top d-flex align-items-center justify-content-between gap-3"> <div class="mt-4 pt-3 border-top d-flex align-items-center justify-content-between gap-2 flex-wrap">
<span class="badge bg-label-success">{{ $priceCard['caption'] }}</span> <span class="badge bg-label-{{ $priceCard['variant'] }}">{{ $priceCard['records'] > 0 ? 'Rata-rata harga' : 'Belum ada data' }}</span>
<small class="text-body-secondary">{{ number_format($priceCard['records'], 0, ',', '.') }} data harga</small> <small class="text-body-secondary">{{ number_format($priceCard['records'], 0, ',', '.') }} data harga</small>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@endforeach
</div>
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#saluran-harga-charts"
aria-expanded="false" aria-controls="saluran-harga-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Saluran &amp; Harga</h5>
<p class="mb-0 text-body-secondary">Saluran penjualan, hubungan dengan pembeli, waktu penjualan, akses ke saluran alternatif, dan tren harga.</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>
@endif </div>
@php
$chartDefinitions = [
'saluranPenjualan' => ['title' => 'Saluran Penjualan Utama', 'description' => 'Kepada siapa petani biasanya menjual hasil panen?', 'variant' => 'success'],
'hubunganPembeli' => ['title' => 'Sifat Hubungan dengan Pembeli', 'description' => 'Hubungan jual petani dengan pembeli utama saat ini.', 'variant' => 'primary'],
'waktuPenjualan' => ['title' => 'Waktu Penjualan Hasil Panen', 'description' => 'Kapan petani biasanya menjual hasil panen?', 'variant' => 'warning'],
'aksesSaluranLain' => ['title' => 'Pengalaman & Akses ke Saluran Lain', 'description' => 'Pengalaman menjual ke tempat lain dan akses yang masih dimiliki.', 'variant' => 'info'],
];
@endphp
<div class="collapse chart-group-panel" id="saluran-harga-charts">
<div class="row g-4 mb-4">
@foreach ($chartDefinitions as $key => $definition)
<div class="col-12 col-lg-6">
<div class="card pasar-chart-card h-100">
<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" id="title-{{ $key }}">{{ $definition['title'] }}</h5>
<small class="text-body-secondary">{{ $definition['description'] }}</small>
</div>
</div>
<div class="card-body d-flex flex-column">
<div id="chart-{{ $key }}" class="pasar-chart" role="img" aria-labelledby="title-{{ $key }}" aria-describedby="summary-{{ $key }}"></div>
<span id="summary-{{ $key }}" class="visually-hidden">
@foreach ($marketCharts[$key]['labels'] as $index => $label)
{{ $label }}: {{ $marketCharts[$key]['series'][$index] }} responden ({{ $marketCharts[$key]['percentages'][$index] }}%).
@endforeach
</span>
</div>
</div>
</div>
@endforeach
@forelse ($priceTrend['commodities'] as $commodity)
<div class="col-12 col-lg-6">
<div class="card pasar-chart-card h-100">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2" id="title-tren-harga-{{ $commodity['id'] }}">Tren Harga 3 Musim — {{ $commodity['name'] }}</h5>
<small class="text-body-secondary">Rata-rata harga jual per bentuk produk selama tiga periode panen (Rp/kg).</small>
</div>
</div>
<div class="card-body">
<div id="chart-tren-harga-{{ $commodity['id'] }}" class="pasar-chart" role="img" aria-labelledby="title-tren-harga-{{ $commodity['id'] }}" aria-describedby="summary-tren-harga-{{ $commodity['id'] }}"></div>
<span id="summary-tren-harga-{{ $commodity['id'] }}" class="visually-hidden"></span>
</div>
</div>
</div>
@empty
<div class="col-12">
<div class="card pasar-chart-card">
<div class="card-header">
<div class="card-title mb-0"><h5 class="m-0">Tren Harga 3 Musim</h5></div>
</div>
<div class="card-body pasar-chart-empty">Belum ada komoditas untuk filter yang dipilih.</div>
</div>
</div>
@endforelse
</div>
</div>
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#kesiapan-premium-charts"
aria-expanded="false" aria-controls="kesiapan-premium-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Kesiapan Pasar Premium</h5>
<p class="mb-0 text-body-secondary">Kesadaran petani tentang pasar premium, persyaratan masuk, dan pengalaman mengaksesnya.</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="kesiapan-premium-charts">
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card pasar-chart-card">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2" id="title-kesadaranPremium">Kesadaran Pasar Premium</h5>
<small class="text-body-secondary">Pengetahuan tentang pasar premium dan persyaratan yang dibutuhkan untuk masuk ke pasar tersebut.</small>
</div>
</div>
<div class="card-body">
<div id="chart-kesadaranPremium" class="pasar-chart" role="img" aria-labelledby="title-kesadaranPremium" aria-describedby="summary-kesadaranPremium"></div>
<span id="summary-kesadaranPremium" class="visually-hidden">
@foreach ($premiumAwareness['fullLabels'] as $index => $label)
{{ $label }}: {{ $premiumAwareness['series'][$index] }} responden ({{ $premiumAwareness['percentages'][$index] }}%).
@endforeach
</span>
</div>
</div>
</div>
</div>
</div>
<section class="mt-4" aria-labelledby="international-market-title" id="international-market">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
<h5 id="international-market-title" class="mb-0">Harga Komoditas Internasional</h5>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" data-market-prev aria-label="Harga sebelumnya">&larr;</button>
<button type="button" class="btn btn-sm btn-icon btn-outline-secondary" data-market-pause aria-pressed="false" aria-label="Jeda slider" title="Jeda slider"><i class="icon-base ti tabler-player-pause" aria-hidden="true"></i></button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-market-next aria-label="Harga berikutnya">&rarr;</button>
</div>
</div>
<p class="text-body-secondary small mb-2">Setiap kartu menampilkan satu komoditas dari satu sumber. Periode dan jenis harga mengikuti penyedia; tidak mengikuti filter wilayah.</p>
<p class="small text-body-secondary" data-market-status role="status">Memuat harga internasional...</p>
<div class="international-market-track" tabindex="0" role="region" aria-roledescription="carousel" aria-label="Harga komoditas menurut sumber"></div>
<template id="international-market-card">
<article class="card card-border-shadow-info international-market-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-2">
<h6 class="mb-0" data-name></h6>
<button type="button" class="market-info-button" data-market-info data-update-info aria-label="Informasi pembaruan harga" aria-expanded="false">!</button>
</div>
<div class="small">Sumber: <a data-source target="_blank" rel="noopener noreferrer"></a></div>
</div>
<span class="international-provider-logo flex-shrink-0" data-provider-logo-wrap hidden>
<img data-provider-logo width="32" height="32" alt="" decoding="async">
</span>
</div>
<div class="pasar-kpi-value" data-price></div>
<div class="small text-body-secondary mt-1" data-unit></div>
<div class="d-flex align-items-center gap-2 mt-3 pt-3 border-top">
<div class="fw-semibold fs-5" data-rupiah></div>
<button type="button" class="market-info-button" data-market-info data-fx-info aria-label="Informasi kurs konversi" aria-expanded="false">!</button>
</div>
<p class="small text-body-secondary mt-2 mb-2" data-description></p>
<div class="small text-body-secondary" data-period></div>
</div>
</article>
</template>
</section>
</div> </div>
@endsection @endsection
@push('scripts') @push('scripts')
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const marketSection = document.getElementById('international-market');
const marketTrack = marketSection.querySelector('.international-market-track');
const marketStatus = marketSection.querySelector('[data-market-status]');
const pauseButton = marketSection.querySelector('[data-market-pause]');
let activeMarketInfo = null;
function closeMarketInfo() {
if (!activeMarketInfo) return;
activeMarketInfo.button.setAttribute('aria-expanded', 'false');
activeMarketInfo.popover.dispose();
activeMarketInfo = null;
}
document.addEventListener('click', event => {
const button = event.target.closest('[data-market-info]');
if (button && marketSection.contains(button)) {
const same = activeMarketInfo?.button === button;
closeMarketInfo();
if (same) return;
const popover = new bootstrap.Popover(button, {
container: 'body', placement: 'auto', trigger: 'manual', html: false,
title: button.hasAttribute('data-fx-info') ? 'Kurs konversi' : 'Pembaruan harga',
content: button.dataset.info || 'Informasi belum tersedia.'
});
popover.show();
button.setAttribute('aria-expanded', 'true');
activeMarketInfo = { button, popover };
} else if (!event.target.closest('.popover')) closeMarketInfo();
});
document.addEventListener('keydown', event => {
if (event.key === 'Escape') closeMarketInfo();
});
marketTrack.addEventListener('focusout', event => {
if (activeMarketInfo?.button === event.target) closeMarketInfo();
});
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let marketPaused = reducedMotion.matches;
let marketSignature = '';
const quoteDateFormat = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium', timeStyle: 'long', timeZone: 'Asia/Jakarta'
});
const rupiahFormat = new Intl.NumberFormat('id-ID', {
style: 'currency', currency: 'IDR', maximumFractionDigits: 2
});
function syncMarketPause() {
const label = marketPaused ? 'Putar slider' : 'Jeda slider';
pauseButton.querySelector('i').className = 'icon-base ti ' + (marketPaused ? 'tabler-player-play' : 'tabler-player-pause');
pauseButton.setAttribute('aria-label', label);
pauseButton.title = label;
pauseButton.setAttribute('aria-pressed', String(marketPaused));
}
syncMarketPause();
pauseButton.addEventListener('click', () => { marketPaused = !marketPaused; syncMarketPause(); });
let marketCycleWidth = 0;
let lastMarketFrame = null;
function measureMarketCycle() {
const first = marketTrack.firstElementChild;
const duplicate = marketTrack.querySelector('[data-market-clone]');
marketCycleWidth = first && duplicate ? duplicate.offsetLeft - first.offsetLeft : 0;
}
function slideMarket(direction) {
if (!marketTrack.firstElementChild) return;
const step = marketTrack.firstElementChild.getBoundingClientRect().width + 20;
if (direction < 0 && marketTrack.scrollLeft < step && marketCycleWidth) {
marketTrack.scrollLeft += marketCycleWidth;
}
marketTrack.scrollLeft += step * direction;
}
marketSection.querySelector('[data-market-prev]').addEventListener('click', () => slideMarket(-1));
marketSection.querySelector('[data-market-next]').addEventListener('click', () => slideMarket(1));
window.addEventListener('resize', measureMarketCycle);
function animateMarket(timestamp) {
const elapsed = lastMarketFrame === null ? 0 : Math.min(timestamp - lastMarketFrame, 64);
lastMarketFrame = timestamp;
if (marketCycleWidth > 0) {
if (!marketPaused && !document.hidden && !marketTrack.contains(document.activeElement)) {
// Keep subpixel progress independently: scrollLeft may round tiny per-frame changes.
marketScrollPosition += elapsed * 0.028;
marketTrack.scrollLeft = marketScrollPosition;
} else {
marketScrollPosition = marketTrack.scrollLeft;
}
if (marketTrack.scrollLeft >= marketCycleWidth) {
marketScrollPosition = marketTrack.scrollLeft - marketCycleWidth;
marketTrack.scrollLeft = marketScrollPosition;
}
}
window.requestAnimationFrame(animateMarket);
}
let marketScrollPosition = 0;
marketTrack.addEventListener('scroll', () => {
if (Math.abs(marketTrack.scrollLeft - marketScrollPosition) > 2) marketScrollPosition = marketTrack.scrollLeft;
}, { passive: true });
window.requestAnimationFrame(animateMarket);
async function refreshInternationalPrices() {
try {
const response = await fetch(@json(route('dashboard.pasar.international')), {
headers: { 'Accept': 'application/json' }, credentials: 'same-origin',
signal: AbortSignal.timeout(25000)
});
if (!response.ok) throw new Error('Harga tidak tersedia');
const payload = await response.json();
if (!Array.isArray(payload.quotes)) throw new Error('Data tidak valid');
const quotes = payload.quotes.filter(quote => typeof quote.price === 'number' && Number.isFinite(quote.price) && quote.price > 0);
const signature = JSON.stringify([quotes, payload.exchange_rate]);
if (signature !== marketSignature) {
const previousScroll = marketTrack.scrollLeft;
const fragment = document.createDocumentFragment();
quotes.forEach(quote => {
const card = document.getElementById('international-market-card').content.cloneNode(true);
card.querySelector('[data-name]').textContent = quote.name;
card.querySelector('[data-update-info]').dataset.info = quote.update_info || (quote.frequency === 'monthly'
? 'IMF menyediakan harga acuan rata-rata bulanan. Data terbaru diperiksa setiap hari. Periode harga tercantum di kartu.'
: 'Harga Yahoo Finance diperiksa setiap 5 menit. Data dapat tertunda mengikuti penyedia dan jam perdagangan.');
const rate = payload.exchange_rate;
let rateInfo = 'Kurs USD/IDR belum tersedia. Harga asli tetap ditampilkan.';
if (rate && typeof rate.value === 'number') {
rateInfo = '1 USD = ' + rupiahFormat.format(rate.value) + '. Sumber: ' + rate.source + '.';
const rateDate = new Date(rate.quoted_at);
if (!Number.isNaN(rateDate.getTime())) rateInfo += ' Waktu kurs: ' + quoteDateFormat.format(rateDate) + '.';
if (rate.stale) rateInfo += ' Menggunakan kurs tersimpan.';
if (quote.frequency === 'monthly') rateInfo += ' Konversi memakai kurs ini, bukan kurs historis bulan harga tersebut.';
}
card.querySelector('[data-fx-info]').dataset.info = rateInfo;
const providerLogos = {
yahoo: @json(asset('assets/img/providers/yahoo-finance.ico')),
imf: @json(asset('assets/img/providers/imf.ico')),
alpha: @json(asset('assets/img/providers/alpha-vantage.ico'))
};
const logoUrl = providerLogos[(quote.id || '').split(':')[0]];
if (logoUrl) {
const logo = card.querySelector('[data-provider-logo]');
logo.src = logoUrl;
logo.alt = 'Logo ' + quote.source;
card.querySelector('[data-provider-logo-wrap]').hidden = false;
}
const source = card.querySelector('[data-source]');
source.textContent = quote.source;
if (quote.source_url?.startsWith('https://')) source.href = quote.source_url;
card.querySelector('[data-price]').textContent = quote.price.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
card.querySelector('[data-unit]').textContent = quote.unit;
card.querySelector('[data-rupiah]').textContent = typeof quote.price_idr_per_kg === 'number'
? rupiahFormat.format(quote.price_idr_per_kg) + ' / kg' : 'Konversi rupiah belum tersedia';
card.querySelector('[data-description]').textContent = quote.description;
let period = '-';
if (quote.frequency === 'monthly' && /^\d{4}-\d{2}$/.test(quote.period || '')) {
period = 'Rata-rata ' + new Intl.DateTimeFormat('id-ID', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(quote.period + '-01T00:00:00Z'));
} else if (quote.period) {
const date = new Date(quote.period);
if (!Number.isNaN(date.getTime())) period = 'Waktu harga: ' + quoteDateFormat.format(date);
}
card.querySelector('[data-period]').textContent = period + (quote.stale ? ' · Data tersimpan' : '');
fragment.append(card);
});
closeMarketInfo();
marketTrack.replaceChildren(fragment);
if (quotes.length > 1) {
// Repeat the cards so the end joins the start without a visible rewind.
const originals = [...marketTrack.children];
[...originals, ...originals].forEach(original => {
const clone = original.cloneNode(true);
clone.dataset.marketClone = 'true';
marketTrack.append(clone);
});
}
measureMarketCycle();
marketScrollPosition = marketCycleWidth ? previousScroll % marketCycleWidth : 0;
marketTrack.scrollLeft = marketScrollPosition;
marketSignature = signature;
}
marketStatus.textContent = quotes.length
? '' : 'Harga belum tersedia. Pembaruan akan dicoba kembali otomatis.';
marketStatus.hidden = quotes.length > 0;
[pauseButton, ...marketSection.querySelectorAll('[data-market-prev], [data-market-next]')].forEach(button => button.disabled = quotes.length < 2);
} catch (error) {
marketStatus.hidden = false;
marketStatus.textContent = marketTrack.childElementCount
? 'Pembaruan gagal. Menampilkan harga sebelumnya dan akan mencoba kembali.'
: 'Harga belum dapat dimuat. Akan dicoba kembali otomatis.';
} finally {
window.setTimeout(() => {
if (!document.hidden) refreshInternationalPrices();
else waitForVisible();
}, 60000);
}
}
function waitForVisible() {
document.addEventListener('visibilitychange', function onVisible() {
if (document.hidden) return;
document.removeEventListener('visibilitychange', onVisible);
refreshInternationalPrices();
});
}
refreshInternationalPrices();
const wilayah = @json($wilayah); const wilayah = @json($wilayah);
const marketCharts = @json($marketCharts);
const chartDefinitions = @json($chartDefinitions);
const priceTrend = @json($priceTrend);
const premiumAwareness = @json($premiumAwareness);
function escapeHtml(value) {
const element = document.createElement('span');
element.textContent = String(value);
return element.innerHTML;
}
function formatNumber(value, digits = 0) {
return Number(value).toLocaleString('id-ID', { maximumFractionDigits: digits });
}
const compactLabels = {
saluranPenjualan: ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Pembeli langsung/eksportir', 'Lainnya'],
waktuPenjualan: ['Langsung setelah panen', 'Ditumpuk beberapa waktu', 'Saat butuh uang mendesak'],
aksesSaluranLain: ['Tidak pernah', 'Berhenti, tanpa akses', 'Berhenti, bisa kembali', 'Sesekali masih menjual', 'Akses alternatif aktif'],
kesadaranPremium: ['Belum pernah mendengar', 'Belum paham persyaratan', 'Sedikit paham persyaratan', 'Paham, belum mengakses', 'Akses premium konsisten'],
};
const mobileLabels = {
saluranPenjualan: ['Tengkulak/lokal', 'Koperasi', 'BUMDes', 'Pembeli/eksportir', 'Lainnya'],
hubunganPembeli: ['Bebas pindah', 'Langganan tetap', 'Berbasis hutang', 'Titip jual', 'Komitmen volume', 'Semi-kontrak'],
waktuPenjualan: ['Setelah panen', 'Ditumpuk dulu', 'Uang mendesak'],
aksesSaluranLain: ['Tidak pernah', 'Tanpa akses', 'Bisa kembali', 'Sesekali jual', 'Akses aktif'],
kesadaranPremium: ['Belum mendengar', 'Belum paham syarat', 'Sedikit paham', 'Paham, belum akses', 'Akses konsisten'],
};
function hasTrendData(commodity) {
return commodity && commodity.series.some(function (series) {
return series.data.some(function (value) { return value !== null && Number(value) > 0; });
});
}
function formatPrice(value) {
return value === null || value === undefined ? 'Belum ada data' : 'Rp ' + formatNumber(value) + '/kg';
}
function renderPriceTrend(commodity) {
const element = document.getElementById('chart-tren-harga-' + commodity.id);
const summary = document.getElementById('summary-tren-harga-' + commodity.id);
if (!hasTrendData(commodity) || typeof ApexCharts === 'undefined') {
element.classList.add('pasar-chart-empty');
element.textContent = !hasTrendData(commodity)
? 'Belum ada data harga untuk komoditas dan filter yang dipilih.'
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
return;
}
summary.textContent = commodity.name + '. ' + commodity.series.map(function (series) {
return series.name + ': ' + series.data.map(function (value, index) {
return priceTrend.periods[index] + ' ' + formatPrice(value);
}).join(', ');
}).join('. ');
const colors = config.chartColors.series.slice(0, 4);
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
new ApexCharts(element, {
chart: { type: 'line', height: 360, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false }, zoom: { enabled: false } },
series: commodity.series,
colors: colors,
stroke: { width: 3, curve: 'straight' },
markers: { size: 5, strokeWidth: 2, hover: { sizeOffset: 2 } },
dataLabels: { enabled: false },
xaxis: {
categories: priceTrend.periods,
tickPlacement: 'between',
labels: { rotate: 0, style: labelStyle },
axisBorder: { color: config.chartColors.grid },
axisTicks: { color: config.chartColors.grid },
},
yaxis: {
min: 0,
title: { text: 'Harga (Rp/kg)', style: { color: config.colors.bodyColor, fontFamily: config.fontFamily, fontWeight: 400 } },
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
},
grid: { borderColor: config.chartColors.grid, strokeDashArray: 4, padding: { left: 8, right: 20 } },
legend: { position: 'bottom', fontSize: '13px', fontFamily: config.fontFamily, labels: { colors: config.colors.bodyColor }, itemMargin: { horizontal: 12, vertical: 6 } },
tooltip: {
shared: true,
intersect: false,
custom: function ({ dataPointIndex }) {
const rows = commodity.series.map(function (series, index) {
return `<div class="pasar-trend-tooltip-row"><span style="color:${colors[index]}">${escapeHtml(series.name)}</span><strong>${formatPrice(series.data[dataPointIndex])}</strong></div>`;
}).join('');
return `<div class="pasar-chart-tooltip" style="background:${config.colors.cardColor};color:${config.colors.headingColor}"><strong>${escapeHtml(commodity.name)} — ${escapeHtml(priceTrend.periods[dataPointIndex])}</strong>${rows}</div>`;
},
},
responsive: [{
breakpoint: 576,
options: {
chart: { height: 420 },
legend: { fontSize: '12px', itemMargin: { horizontal: 6, vertical: 4 } },
xaxis: {
labels: {
hideOverlappingLabels: false,
style: { fontSize: '11px' },
formatter: function (value) {
return { 'Dua Tahun Lalu': '2 thn lalu', 'Tahun Lalu': 'Tahun lalu', 'Panen Terakhir': 'Terakhir' }[value] || value;
},
},
},
yaxis: {
title: { text: undefined },
labels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
},
},
}],
}).render();
}
function renderResponseChart(key, data, definition) {
const element = document.getElementById('chart-' + key);
if (!data.respondents || typeof ApexCharts === 'undefined') {
element.classList.add('pasar-chart-empty');
element.textContent = !data.respondents
? 'Belum ada data jawaban untuk filter yang dipilih.'
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
return;
}
const color = config.chartColors.semantic[definition.variant];
const borderColor = config.chartColors.grid;
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
const maxCount = Math.max(...data.series);
const tickCount = Math.min(4, maxCount);
const axisMax = Math.ceil(maxCount * 1.25 / tickCount) * tickCount;
new ApexCharts(element, {
chart: { type: 'bar', height: 330, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false } },
series: [{ name: 'Responden', data: data.series }],
colors: [color],
plotOptions: { bar: { horizontal: true, borderRadius: 5, barHeight: '62%', dataLabels: { position: 'top' } } },
dataLabels: {
enabled: true,
textAnchor: 'start',
offsetX: 6,
style: { fontSize: '12px', fontFamily: config.fontFamily, fontWeight: 500, colors: [config.colors.headingColor] },
formatter: function (value, options) {
return `${formatNumber(value)} (${formatNumber(data.percentages[options.dataPointIndex], 1)}%)`;
},
},
xaxis: {
categories: compactLabels[key] || data.labels,
min: 0,
max: axisMax,
tickAmount: tickCount,
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
axisBorder: { color: borderColor },
axisTicks: { color: borderColor },
},
yaxis: { labels: { maxWidth: 185, style: labelStyle } },
grid: { borderColor: borderColor, strokeDashArray: 4, padding: { right: 20 } },
legend: { show: false },
tooltip: {
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light',
custom: function ({ dataPointIndex }) {
return `<div class="pasar-chart-tooltip" style="background:${color};color:${config.colors.headingColor};font-family:${config.fontFamily}">${escapeHtml(data.fullLabels[dataPointIndex])}: <strong>${formatNumber(data.series[dataPointIndex])} responden (${formatNumber(data.percentages[dataPointIndex], 1)}%)</strong></div>`;
},
},
responsive: [{
breakpoint: 576,
options: {
xaxis: { categories: mobileLabels[key], tickAmount: 2 },
yaxis: { labels: { maxWidth: 125, style: { fontSize: '11px' } } },
grid: { padding: { right: 10 } },
dataLabels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
},
}],
}).render();
}
function renderMarketCharts() {
Object.entries(marketCharts).forEach(function ([key, data]) {
renderResponseChart(key, data, chartDefinitions[key]);
});
priceTrend.commodities.forEach(renderPriceTrend);
}
function setupChartPanel(panelId, renderCharts) {
const chartPanel = document.getElementById(panelId);
const chartControl = document.querySelector('.chart-group-header-toggle[aria-controls="' + chartPanel.id + '"]');
const chartIcon = chartControl.querySelector('.chart-group-icon-toggle i');
let rendered = false;
function syncChartPanelState() {
const expanded = chartPanel.classList.contains('show');
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
chartControl.setAttribute('aria-expanded', String(expanded));
chartControl.setAttribute('aria-label', label);
chartControl.title = label;
chartIcon.classList.toggle('tabler-chevron-up', expanded);
chartIcon.classList.toggle('tabler-chevron-down', !expanded);
}
chartControl.addEventListener('keydown', function (event) {
if (event.target !== chartControl || !['Enter', ' '].includes(event.key)) return;
event.preventDefault();
chartControl.click();
});
chartPanel.addEventListener('shown.bs.collapse', function (event) {
if (event.target !== chartPanel) return;
syncChartPanelState();
if (!rendered) {
renderCharts();
rendered = true;
}
window.dispatchEvent(new Event('resize'));
});
chartPanel.addEventListener('hidden.bs.collapse', function (event) {
if (event.target === chartPanel) syncChartPanelState();
});
syncChartPanelState();
}
setupChartPanel('saluran-harga-charts', renderMarketCharts);
setupChartPanel('kesiapan-premium-charts', function () {
renderResponseChart('kesadaranPremium', premiumAwareness, { variant: 'primary' });
});
function fillSelect($select, items, placeholder) { function fillSelect($select, items, placeholder) {
$select.empty().append(new Option(placeholder, '')); $select.empty().append(new Option(placeholder, ''));
@@ -208,7 +914,7 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
} }
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas').select2({ width: '100%' }); $('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
$('#filter-provinsi').on('change', function () { $('#filter-provinsi').on('change', function () {
const provinsi = selectedProvinsi(); const provinsi = selectedProvinsi();
+1 -1
View File
@@ -7,7 +7,7 @@
<script> <script>
document.write(new Date().getFullYear()); document.write(new Date().getFullYear());
</script> </script>
, by <a href="#" target="_blank" class="footer-link">Yayasan Bhumi Loka Lestari</a> , by <a href="#" target="_blank" class="footer-link">PT Republik Solusi</a>
</div> </div>
</div> </div>
</div> </div>
@@ -31,12 +31,12 @@
<div class="card mb-6"> <div class="card mb-6">
<div class="card-body pt-12"> <div class="card-body pt-12">
<h5 class="pb-3 border-bottom mb-4">Saluran Penjualan & Akses Pasar</h5> <h5 class="pb-4 border-bottom mb-4">Saluran Penjualan & Akses Pasar</h5>
<h6 class="fw-bold text-dark mb-2">Saluran Penjualan Utama</h6> <h5 class="mb-4 mt-4">Saluran Penjualan Utama</h5>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-semibold">144. Kepada siapa Bapak/Ibu biasanya menjual hasil panen? (boleh pilih lebih dari satu)</label> <p class="form-label d-block">Kepada siapa Bapak/Ibu biasanya menjual hasil panen? (boleh pilih lebih dari satu)</p>
<div class="row g-2"> <div class="row">
@php @php
$tujuanOptions = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir']; $tujuanOptions = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir'];
@endphp @endphp
@@ -55,16 +55,16 @@
<input class="form-check-input" type="checkbox" name="tujuan_penjualan[]" value="Lainnya" id="tujuan-lainnya" {{ in_array('Lainnya', (array) old('tujuan_penjualan', $aksesPasar?->tujuan_penjualan ?? []), true) ? 'checked' : '' }}> <input class="form-check-input" type="checkbox" name="tujuan_penjualan[]" value="Lainnya" id="tujuan-lainnya" {{ in_array('Lainnya', (array) old('tujuan_penjualan', $aksesPasar?->tujuan_penjualan ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="tujuan-lainnya">Lainnya:</label> <label class="form-check-label" for="tujuan-lainnya">Lainnya:</label>
</div> </div>
<input type="text" class="form-control form-control-sm w-auto flex-grow-1 max-w-400" style="max-width: 400px;" name="tujuan_penjualan_lainnya" value="{{ old('tujuan_penjualan_lainnya', $aksesPasar?->tujuan_penjualan_lainnya) }}" placeholder="Sebutkan lainnya..."> <input type="text" class="form-control w-auto flex-grow-1 max-w-400" style="max-width: 400px;" name="tujuan_penjualan_lainnya" value="{{ old('tujuan_penjualan_lainnya', $aksesPasar?->tujuan_penjualan_lainnya) }}" placeholder="Sebutkan lainnya...">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<h6 class="fw-bold text-dark mt-4 mb-2">Sifat Hubungan dengan Pembeli</h6> <h5 class="mb-4 mt-4">Sifat Hubungan Dengan Pembeli</h5>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-semibold">Bagaimana sifat hubungan jual Bapak/Ibu dengan pembeli utama saat ini?</label> <p class="form-label d-block">Bagaimana sifat hubungan jual Bapak/Ibu dengan pembeli utama saat ini?</p>
<div class="row g-2"> <div class="row">
@php @php
$sifatHubunganOptions = [ $sifatHubunganOptions = [
'Bebas, bisa pindah pembeli kapan saja', 'Bebas, bisa pindah pembeli kapan saja',
@@ -86,10 +86,10 @@
</div> </div>
</div> </div>
<h6 class="fw-bold text-dark mt-4 mb-2">Bentuk Produk yang Dijual</h6> <h5 class="mb-4 mt-4">Bentuk Produk Yang Dijual</h5>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-semibold">Dalam bentuk apa produk dijual?</label> <p class="form-label d-block">Dalam bentuk apa produk dijual?</p>
<div class="row g-2"> <div class="row">
@php @php
$bentukPenjualanOptions = [ $bentukPenjualanOptions = [
'Buah segar hasil petik (gelondongan)', 'Buah segar hasil petik (gelondongan)',
@@ -109,10 +109,10 @@
</div> </div>
</div> </div>
<h6 class="fw-bold text-dark mt-4 mb-2">Frekuensi Penjualan</h6> <h5 class="mb-4 mt-4">Frekuensi Penjualan</h5>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-semibold">Kapan biasanya menjual hasil panen?</label> <p class="form-label d-block">Kapan biasanya menjual hasil panen?</p>
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column">
@php @php
$frekuensiOptions = [ $frekuensiOptions = [
'Langsung setelah panen', 'Langsung setelah panen',
@@ -121,20 +121,19 @@
]; ];
@endphp @endphp
@foreach($frekuensiOptions as $freq) @foreach($frekuensiOptions as $freq)
<div class="form-check mb-1"> <div class="form-check mb-2">
<input class="form-check-input" type="radio" name="frekuensi_penjualan" value="{{ $freq }}" id="freq-{{ Str::slug($freq) }}" {{ old('frekuensi_penjualan', $aksesPasar?->frekuensi_penjualan) === $freq ? 'checked' : '' }}> <input class="form-check-input" type="radio" name="frekuensi_penjualan" value="{{ $freq }}" id="freq-{{ Str::slug($freq) }}" {{ old('frekuensi_penjualan', $aksesPasar?->frekuensi_penjualan) === $freq ? 'checked' : '' }}>
<label class="form-check-label" for="freq-{{ Str::slug($freq) }}">{{ $freq }}</label> <label class="form-check-label" for="freq-{{ Str::slug($freq) }}">{{ $freq }}</label>
</div> </div>
@endforeach @endforeach
</div> </div>
</div> </div>
<hr class="my-4">
<h5 class="pb-3 border-bottom mb-4">Pengalaman Saluran Lain</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Pengalaman Saluran Lain</h5>
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-12 mb-4 form-control-validation">
<label class="form-label d-block fw-bold">Pengalaman & akses ke saluran lain</label> <label class="form-label d-block">Apakah pernah menjual ke tempat lain sebelumnya?</label>
@foreach($skorPengalamanOptions as $option) @foreach($skorPengalamanOptions as $option)
<div class="form-check mb-2"> <div class="form-check mb-2">
<input class="form-check-input @error('skor_pengalaman') is-invalid @enderror" type="radio" <input class="form-check-input @error('skor_pengalaman') is-invalid @enderror" type="radio"
@@ -146,41 +145,67 @@
@error('skor_pengalaman')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror @error('skor_pengalaman')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-12" id="blok-saluran-lanjutan">
<label class="form-label" for="alasan-berhenti">Alasan berhenti</label> <div class="row">
<input type="text" class="form-control @error('alasan_berhenti') is-invalid @enderror" id="alasan-berhenti" name="alasan_berhenti" value="{{ old('alasan_berhenti', $aksesPasar?->alasan_berhenti) }}" placeholder="Opsional (jika sudah berhenti)...">
@error('alasan_berhenti')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block mb-2" for="saluran-terdahulu">Dulu pernah jual ke?</label> <label class="form-label" for="alasan-berhenti">Alasan berhenti</label>
@foreach($saluranTerdahuluOptions as $option) <input type="text" class="form-control @error('alasan_berhenti') is-invalid @enderror" id="alasan-berhenti" name="alasan_berhenti" value="{{ old('alasan_berhenti', $aksesPasar?->alasan_berhenti) }}" placeholder="Opsional (jika sudah berhenti)...">
<div class="form-check mb-2"> @error('alasan_berhenti')<div class="invalid-feedback">{{ $message }}</div>@enderror
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
type="checkbox"
id="saluran-terdahulu-{{ Str::slug($option['value']) }}"
name="saluran_terdahulu[]"
value="{{ $option['value'] }}"
{{ in_array($option['value'], (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="saluran-terdahulu-{{ Str::slug($option['value']) }}">
{{ $option['label'] }}
</label>
</div> </div>
@endforeach
@error('saluran_terdahulu')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-6 mb-4"></div>
<label class="form-label" for="saluran-lainnya">Lainnya</label>
<input type="text" class="form-control @error('saluran_terdahulu_lainnya') is-invalid @enderror" id="saluran-lainnya" name="saluran_terdahulu_lainnya" value="{{ old('saluran_terdahulu_lainnya', $aksesPasar?->saluran_terdahulu_lainnya) }}" placeholder="Sebutkan saluran lainnya..."> <div class="col-md-12 mb-4 form-control-validation">
@error('saluran_terdahulu_lainnya')<div class="invalid-feedback">{{ $message }}</div>@enderror <label class="form-label d-block" for="saluran-terdahulu">Jika pernah, dulu menjual ke mana? (boleh pilih lebih dari satu)</label>
<div class="row">
@foreach($saluranTerdahuluOptions as $option)
@if($option['value'] !== $saluranLainnyaValue)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
type="checkbox"
id="saluran-terdahulu-{{ Str::slug($option['value']) }}"
name="saluran_terdahulu[]"
value="{{ $option['value'] }}"
{{ in_array($option['value'], (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="saluran-terdahulu-{{ Str::slug($option['value']) }}">
{{ $option['label'] }}
</label>
</div>
</div>
@endif
@endforeach
<div class="col-12 mt-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="form-check mb-0">
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
type="checkbox"
id="saluran-terdahulu-lainnya-checkbox"
name="saluran_terdahulu[]"
value="{{ $saluranLainnyaValue }}"
{{ in_array($saluranLainnyaValue, (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label text-nowrap" for="saluran-terdahulu-lainnya-checkbox">
Lainnya:
</label>
</div>
<input type="text" class="form-control w-auto flex-grow-1 @error('saluran_terdahulu_lainnya') is-invalid @enderror" style="max-width: 400px;" id="saluran-lainnya" name="saluran_terdahulu_lainnya" value="{{ old('saluran_terdahulu_lainnya', $aksesPasar?->saluran_terdahulu_lainnya) }}" placeholder="Sebutkan saluran lainnya...">
</div>
</div>
</div>
@error('saluran_terdahulu')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
@error('saluran_terdahulu_lainnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
</div>
</div> </div>
</div> </div>
<h5 class="pb-3 border-bottom mb-4 mt-4">HARGA DI SETIAP TITIK</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Harga Di Setiap Titik</h5>
<h6 class="mb-2">Harga Jual Panen Terakhir</h6> <h5 class="mb-4 mt-4">Harga Jual Panen Terakhir</h5>
<p class="mb-1">Harga jual komoditas tiga musim panen terakhir:</p> <p class="form-label d-block">Harga jual komoditas tiga musim panen terakhir:</p>
<p class="mb-4">Dalam tiga musim panen terakhir, berapa harga jual yang Bapak/Ibu terima untuk komoditas ini sesuai dengan bentuk produk yang dijual?</p> <p class="form-label d-block">Dalam tiga musim panen terakhir, berapa harga jual yang Bapak/Ibu terima untuk komoditas ini sesuai dengan bentuk produk yang dijual?</p>
<div class="table-responsive mb-4"> <div class="table-responsive mb-4">
<table class="table table-bordered align-middle"> <table class="table table-bordered align-middle">
@@ -197,7 +222,7 @@
<tr> <tr>
<td> <td>
<input type="hidden" name="harga_jual[{{ $row['index'] }}][bentuk_produk]" value="{{ $row['bentuk'] }}"> <input type="hidden" name="harga_jual[{{ $row['index'] }}][bentuk_produk]" value="{{ $row['bentuk'] }}">
<strong>{{ $row['bentuk'] }}:</strong> {{ $row['bentuk'] }}:
</td> </td>
<td> <td>
<div class="input-group"> <div class="input-group">
@@ -226,18 +251,87 @@
</table> </table>
</div> </div>
<h5 class="pb-3 border-bottom mb-4 mt-4">PELUANG NILAI TAMBAH</h5> <h5 class="mb-4 mt-4">Sumber Informasi Harga</h5>
<div class="mb-4">
<div class="border rounded p-4 mb-4">
<div class="mb-4"> <div class="mb-4">
<h6 class="mb-1 text-primary">Detail Praktik Fermentasi</h6> <p class="form-label d-block">Sebelum menjual, apakah Bapak/Ibu sudah tahu harga yang akan diterima?</p>
<p class="text-muted small mb-0">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang Ibu/Bapak lakukan?</p> <div class="row">
@php
$tahuHargaOptions = [
'Selalu tahu sebelum pergi jual',
'Kadang tahu, kadang tidak',
'Baru tahu saat sudah di tempat pembeli',
'Tidak pernah tahu sebelumnya — langsung jual saja'
];
@endphp
@foreach($tahuHargaOptions as $tahu)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input @error('tahu_harga_sebelumnya') is-invalid @enderror" type="radio" name="tahu_harga_sebelumnya" value="{{ $tahu }}" id="tahu-{{ Str::slug($tahu) }}" {{ old('tahu_harga_sebelumnya', $aksesPasar?->tahu_harga_sebelumnya) === $tahu ? 'checked' : '' }}>
<label class="form-check-label" for="tahu-{{ Str::slug($tahu) }}">{{ $tahu }}</label>
</div>
</div>
@endforeach
</div>
@error('tahu_harga_sebelumnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
<div class="mb-0">
<p class="form-label d-block">Dari mana biasanya Bapak/Ibu tahu harga hasil panen sebelum menjual? (boleh pilih lebih dari satu)</p>
<div class="row">
@php
$sumberInfoHargaOptions = [
'Tengkulak/pedagang yang datang langsung',
'Sesama petani/tetangga',
'PPL/petugas dinas',
'HP/WhatsApp/internet',
'Radio/TV',
'Facebook',
'Tiktok',
'Instagram',
'Tidak pernah cek — langsung jual saja'
];
$sumberInfoLainnyaValue = 'Lainnya';
@endphp
@foreach($sumberInfoHargaOptions as $sumber)
<div class="col-md-4">
<div class="form-check mb-2">
<input class="form-check-input @error('sumber_info_harga') is-invalid @enderror" type="checkbox" name="sumber_info_harga[]" value="{{ $sumber }}" id="sumber-{{ Str::slug($sumber) }}" {{ in_array($sumber, (array) old('sumber_info_harga', $aksesPasar?->sumber_info_harga ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="sumber-{{ Str::slug($sumber) }}">{{ $sumber }}</label>
</div>
</div>
@endforeach
<div class="col-12 mt-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="form-check mb-0">
<input class="form-check-input @error('sumber_info_harga') is-invalid @enderror" type="checkbox" name="sumber_info_harga[]" value="{{ $sumberInfoLainnyaValue }}" id="sumber-lainnya-checkbox" {{ in_array($sumberInfoLainnyaValue, (array) old('sumber_info_harga', $aksesPasar?->sumber_info_harga ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label text-nowrap" for="sumber-lainnya-checkbox">Lainnya:</label>
</div>
<input type="text" class="form-control w-auto flex-grow-1 @error('sumber_info_harga_lainnya') is-invalid @enderror" style="max-width: 400px;" id="sumber-info-lainnya-input" name="sumber_info_harga_lainnya" value="{{ old('sumber_info_harga_lainnya', $aksesPasar?->sumber_info_harga_lainnya) }}" placeholder="Sebutkan sumber lainnya...">
</div>
</div>
</div>
@error('sumber_info_harga')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
@error('sumber_info_harga_lainnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Peluang Nilai Tambah</h5>
<div class="mb-4">
<div class="mb-4">
<h5 class="mb-4 mt-4">Detail Praktik Fermentasi</h5>
<p class="form-label d-block">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang Ibu/Bapak lakukan?</p>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column"> <div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
<div class="mb-2 d-flex align-items-end"> <div class="d-flex align-items-end">
<label class="form-label mb-0" for="praktik-fermentasi">Praktik fermentasi yang dilakukan</label> <label class="form-label" for="praktik-fermentasi">Praktik fermentasi yang dilakukan</label>
</div> </div>
<div> <div>
@foreach($praktikFermentasiOptions as $option) @foreach($praktikFermentasiOptions as $option)
@@ -258,8 +352,8 @@
</div> </div>
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column"> <div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
<div class="mb-2 d-flex align-items-end"> <div class="d-flex align-items-end">
<label class="form-label mb-0" for="proses-fermentasi">Jika ya, bagaimana prosesnya?</label> <label class="form-label" for="proses-fermentasi">Jika ya, bagaimana prosesnya?</label>
</div> </div>
<div> <div>
<textarea class="form-control @error('proses_fermentasi') is-invalid @enderror" id="proses-fermentasi" name="proses_fermentasi" rows="2" placeholder="Ceritakan prosesnya di sini...">{{ old('proses_fermentasi', $aksesPasar?->proses_fermentasi) }}</textarea> <textarea class="form-control @error('proses_fermentasi') is-invalid @enderror" id="proses-fermentasi" name="proses_fermentasi" rows="2" placeholder="Ceritakan prosesnya di sini...">{{ old('proses_fermentasi', $aksesPasar?->proses_fermentasi) }}</textarea>
@@ -268,8 +362,8 @@
</div> </div>
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column"> <div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
<div class="mb-2 d-flex align-items-end"> <div class="d-flex align-items-end">
<label class="form-label mb-0" for="model-fermentasi">Menurut Bapak/Ibu, apa model yang paling memungkinkan?</label> <label class="form-label" for="model-fermentasi">Menurut Bapak/Ibu, apa model yang paling memungkinkan?</label>
</div> </div>
<div> <div>
<textarea class="form-control @error('model_fermentasi_memungkinkan') is-invalid @enderror" id="model-fermentasi" name="model_fermentasi_memungkinkan" rows="2" placeholder="Model fermentasinya...">{{ old('model_fermentasi_memungkinkan', $aksesPasar?->model_fermentasi_memungkinkan) }}</textarea> <textarea class="form-control @error('model_fermentasi_memungkinkan') is-invalid @enderror" id="model-fermentasi" name="model_fermentasi_memungkinkan" rows="2" placeholder="Model fermentasinya...">{{ old('model_fermentasi_memungkinkan', $aksesPasar?->model_fermentasi_memungkinkan) }}</textarea>
@@ -279,11 +373,11 @@
</div> </div>
</div> </div>
<h5 class="pb-3 border-bottom mb-4 mt-4">AKSES PASAR PREMIUM</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Akses Pasar Premium</h5>
<h6 class="mb-3">Kesadaran Pasar Premium*</h6> <h5 class="mb-4 mt-4">Kesadaran Pasar Premium*</h5>
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block mb-2" for="skor-premium">Pengetahuan & akses pasar premium</label> <label class="form-label d-block" for="skor-premium">Pengetahuan & akses pasar premium</label>
@foreach($skorKesadaranPremiumOptions as $option) @foreach($skorKesadaranPremiumOptions as $option)
<div class="form-check mb-2"> <div class="form-check mb-2">
<input class="form-check-input @error('skor_kesadaran_premium') is-invalid @enderror" <input class="form-check-input @error('skor_kesadaran_premium') is-invalid @enderror"
@@ -301,16 +395,16 @@
</div> </div>
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label mb-2" for="sumber-info">Sumber informasi utama</label> <label class="form-label" for="sumber-info">Sumber informasi utama</label>
<input type="text" class="form-control @error('sumber_info_premium') is-invalid @enderror" id="sumber-info" name="sumber_info_premium" value="{{ old('sumber_info_premium', $aksesPasar?->sumber_info_premium) }}"> <input type="text" class="form-control @error('sumber_info_premium') is-invalid @enderror" id="sumber-info" name="sumber_info_premium" value="{{ old('sumber_info_premium', $aksesPasar?->sumber_info_premium) }}">
@error('sumber_info_premium')<div class="invalid-feedback">{{ $message }}</div>@enderror @error('sumber_info_premium')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
</div> </div>
<h5 class="pb-3 border-bottom mb-4 mt-4">KEPERCAYAAN & PERSEPSI RISIKO</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Kepercayaan & Persepsi Risiko</h5>
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block mb-2" for="tingkat-kepercayaan">Seberapa percaya Bapak/Ibu untuk menjual ke pembeli atau pasar baru, di luar pembeli yang biasa?</label> <label class="form-label d-block" for="tingkat-kepercayaan">Seberapa percaya Bapak/Ibu untuk menjual ke pembeli atau pasar baru, di luar pembeli yang biasa?</label>
@foreach($tingkatKepercayaanOptions as $option) @foreach($tingkatKepercayaanOptions as $option)
<div class="form-check mb-2"> <div class="form-check mb-2">
<input class="form-check-input @error('tingkat_kepercayaan_pembeli_baru') is-invalid @enderror" <input class="form-check-input @error('tingkat_kepercayaan_pembeli_baru') is-invalid @enderror"
@@ -328,7 +422,7 @@
</div> </div>
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label mb-2" for="alasan-kepercayaan">Apa yang membuat Bapak/Ibu ragu atau yakin untuk mencoba pembeli baru?</label> <label class="form-label" for="alasan-kepercayaan">Apa yang membuat Bapak/Ibu ragu atau yakin untuk mencoba pembeli baru?</label>
<textarea class="form-control @error('alasan_kepercayaan_pembeli_baru') is-invalid @enderror" <textarea class="form-control @error('alasan_kepercayaan_pembeli_baru') is-invalid @enderror"
id="alasan-kepercayaan" id="alasan-kepercayaan"
name="alasan_kepercayaan_pembeli_baru" name="alasan_kepercayaan_pembeli_baru"
@@ -341,7 +435,7 @@
<div class="mt-4 text-end"> <div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a> <a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-device-floppy me-1_5 icon-sm"></i>Simpan Akses Pasar <i class="icon-base ti tabler-check me-1_5 icon-sm"></i>Simpan
</button> </button>
</div> </div>
</div> </div>
@@ -359,6 +453,8 @@
const saluranLainnyaValue = @json($saluranLainnyaValue); const saluranLainnyaValue = @json($saluranLainnyaValue);
const praktikBelumPernahValue = String(@json($praktikBelumPernahValue)); const praktikBelumPernahValue = String(@json($praktikBelumPernahValue));
const skorPengalamanBerhentiValues = @json($skorPengalamanBerhentiValues ?? [2,3]).map(Number); const skorPengalamanBerhentiValues = @json($skorPengalamanBerhentiValues ?? [2,3]).map(Number);
const skorBelumPernahValue = "1";
const sumberInfoLainnyaValue = @json($sumberInfoLainnyaValue ?? 'Lainnya');
function onlyDigits(value) { function onlyDigits(value) {
return String(value || '').replace(/\D/g, ''); return String(value || '').replace(/\D/g, '');
@@ -369,6 +465,22 @@
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); return digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
} }
function toggleSaluranLanjutan() {
const selectedRadio = $('input[name="skor_pengalaman"]:checked');
const val = selectedRadio.val();
const labelText = selectedRadio.parent().text().toLowerCase();
const isBelumPernah = !val || labelText.includes('belum pernah') || val === skorBelumPernahValue;
if (isBelumPernah) {
$('#blok-saluran-lanjutan').addClass('d-none');
$('#blok-saluran-lanjutan input[type="text"]').val('');
$('#blok-saluran-lanjutan input[type="checkbox"]').prop('checked', false);
} else {
$('#blok-saluran-lanjutan').removeClass('d-none');
}
}
function syncAlasanBerhenti() { function syncAlasanBerhenti() {
const val = Number($('input[name="skor_pengalaman"]:checked').val()); const val = Number($('input[name="skor_pengalaman"]:checked').val());
const isBerhenti = skorPengalamanBerhentiValues.includes(val); const isBerhenti = skorPengalamanBerhentiValues.includes(val);
@@ -382,6 +494,12 @@
if (!isLainnya) $('#saluran-lainnya').val(''); if (!isLainnya) $('#saluran-lainnya').val('');
} }
function syncSumberInfoLainnya() {
const isLainnya = $('input[name="sumber_info_harga[]"][value="' + sumberInfoLainnyaValue + '"]').is(':checked');
$('#sumber-info-lainnya-input').prop('disabled', !isLainnya);
if (!isLainnya) $('#sumber-info-lainnya-input').val('');
}
function syncPraktikFermentasi() { function syncPraktikFermentasi() {
const isBelumPernah = $('input[name="praktik_fermentasi"]:checked').val() === praktikBelumPernahValue; const isBelumPernah = $('input[name="praktik_fermentasi"]:checked').val() === praktikBelumPernahValue;
$('#proses-fermentasi, #model-fermentasi').prop('disabled', isBelumPernah); $('#proses-fermentasi, #model-fermentasi').prop('disabled', isBelumPernah);
@@ -390,9 +508,15 @@
} }
} }
$('input[name="skor_pengalaman"]').on('change', syncAlasanBerhenti); $('input[name="skor_pengalaman"]').on('change', function () {
toggleSaluranLanjutan();
syncAlasanBerhenti();
});
$('input[name="saluran_terdahulu[]"]').on('change', syncSaluranLainnya); $('input[name="saluran_terdahulu[]"]').on('change', syncSaluranLainnya);
$('input[name="sumber_info_harga[]"]').on('change', syncSumberInfoLainnya);
$('input[name="praktik_fermentasi"]').on('change', syncPraktikFermentasi); $('input[name="praktik_fermentasi"]').on('change', syncPraktikFermentasi);
$(document).on('input', '.rupiah-input', function () { $(document).on('input', '.rupiah-input', function () {
const cursorAtEnd = this.selectionStart === this.value.length; const cursorAtEnd = this.selectionStart === this.value.length;
this.value = formatRupiah(this.value); this.value = formatRupiah(this.value);
@@ -401,12 +525,15 @@
this.setSelectionRange(this.value.length, this.value.length); this.setSelectionRange(this.value.length, this.value.length);
} }
}); });
$('.rupiah-input').each(function () { $('.rupiah-input').each(function () {
this.value = formatRupiah(this.value); this.value = formatRupiah(this.value);
}); });
toggleSaluranLanjutan();
syncAlasanBerhenti(); syncAlasanBerhenti();
syncSaluranLainnya(); syncSaluranLainnya();
syncSumberInfoLainnya();
syncPraktikFermentasi(); syncPraktikFermentasi();
}); });
</script> </script>
@@ -1,37 +1,51 @@
{{-- @php
Partial notif per-card. Dipakai di dalam tiap <div class="card"> supaya notif sukses/gagal $notificationTypes = [
muncul tepat di atas card yang barusan disubmit, bukan di paling atas halaman. 'success' => 'success',
'error' => 'danger',
'warning' => 'warning',
'info' => 'info',
'status' => 'info',
'message' => 'info',
];
$hasNotifications = $errors->any() || collect(array_keys($notificationTypes))->contains(fn ($key) => session()->has($key));
@endphp
Params: @if($hasNotifications)
- $section (string) kunci section, harus sama dengan yang dipakai controller di redirectToSection(). <div id="kelompok-tani-notifications">
- $errorFields (array, optional) daftar nama field/wildcard (mis. 'aset.*') milik card ini, @foreach($notificationTypes as $key => $type)
dipakai buat nentuin apakah error validasi yang ada punya card ini. @if(session()->has($key))
--}} <div class="alert alert-{{ $type }} alert-dismissible" role="alert">
@if(session('success') && session('success_section') === $section) {{ session($key) }}
<div class="alert alert-success alert-dismissible" role="alert"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Tutup"></button>
{{ session('success') }} </div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> @endif
</div> @endforeach
@endif
@if(session('error') && session('error_section') === $section) @if($errors->any())
<div class="alert alert-danger alert-dismissible" role="alert"> <div class="alert alert-danger alert-dismissible" role="alert">
{{ session('error') }} <ul class="mb-0">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> @foreach($errors->all() as $error)
</div> <li>{{ $error }}</li>
@endif
@if(!empty($errorFields) && $errors->has($errorFields))
<div class="alert alert-danger alert-dismissible" role="alert">
<ul class="mb-0">
@foreach($errorFields as $field)
@foreach($errors->get($field) as $messages)
@foreach((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach @endforeach
@endforeach </ul>
@endforeach <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Tutup"></button>
</ul> </div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> @endif
</div> </div>
@push('scripts')
<script>
window.addEventListener('load', function () {
const hasErrors = @json($errors->any() || session()->has('error'));
if (!hasErrors && window.location.hash.startsWith('#section-')) {
const target = document.getElementById(window.location.hash.substring(1));
if (target && target.closest('.kelompok-tani-page') && target.getClientRects().length) {
target.scrollIntoView({ block: 'start' });
return;
}
}
document.getElementById('kelompok-tani-notifications')?.scrollIntoView({ block: 'start' });
});
</script>
@endpush
@endif @endif
@@ -1,31 +1,21 @@
@extends('layout.main') @extends('layout.main')
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y kelompok-tani-page">
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => null]) @include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => null])
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card mb-6"> <div class="card mb-6">
<div class="card-body pt-6"> <div class="card-body pt-6">
<form id="addKelompokTaniForm" action="{{ route('kelompok-tani.store') }}" method="POST"> <form novalidate id="addKelompokTaniForm" action="{{ route('kelompok-tani.store') }}" method="POST">
@csrf @csrf
<h5 class="pb-4 border-bottom mb-4">Data Responden</h5> <h5 class="pb-4 border-bottom mb-4">Data Responden</h5>
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="add-kt-nama-responden">Nama Responden<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-nama-responden">Nama Responden<span class="text-danger">*</span></label>
<input type="text" class="form-control @error('nama_responden') is-invalid @enderror" id="add-kt-nama-responden" name="nama_responden" <input type="text" class="form-control @error('nama_responden') is-invalid @enderror" id="add-kt-nama-responden" name="nama_responden" required
placeholder="Cth: John Doe" value="{{ old('nama_responden') }}" /> placeholder="Cth: John Doe" value="{{ old('nama_responden') }}" />
@error('nama_responden')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('nama_responden')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
@@ -47,7 +37,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="add-kt-nama-kelompok">Nama Kelompok Tani<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-nama-kelompok">Nama Kelompok Tani<span class="text-danger">*</span></label>
<input type="text" class="form-control @error('nama_kelompok') is-invalid @enderror" id="add-kt-nama-kelompok" name="nama_kelompok" <input type="text" class="form-control @error('nama_kelompok') is-invalid @enderror" id="add-kt-nama-kelompok" name="nama_kelompok" required
placeholder="Cth: Kelompok Tani Maju Bersama" value="{{ old('nama_kelompok') }}" /> placeholder="Cth: Kelompok Tani Maju Bersama" value="{{ old('nama_kelompok') }}" />
@error('nama_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('nama_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
@@ -68,38 +58,50 @@
<div class="row"> <div class="row">
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="add-kt-provinsi">Provinsi<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-provinsi">Provinsi<span class="text-danger">*</span></label>
<select class="form-control @error('provinsi_id') is-invalid @enderror" id="add-kt-provinsi" name="provinsi_id"> <select class="form-select @error('provinsi_id') is-invalid @enderror" id="add-kt-provinsi" name="provinsi_id" required>
<option value="">Pilih Provinsi</option> <option value="">Pilih Provinsi</option>
@foreach($provinsi as $p) @foreach($provinsi as $p)
<option value="{{ $p->id }}" {{ old('provinsi_id') == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option> <option value="{{ $p->id }}" {{ old('provinsi_id') == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option>
@endforeach @endforeach
</select> </select>
@error('provinsi_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="add-kt-kabupaten">Kabupaten/Kota<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-kabupaten">Kabupaten/Kota<span class="text-danger">*</span></label>
<select class="form-control @error('kabupaten_id') is-invalid @enderror" id="add-kt-kabupaten" name="kabupaten_id" disabled> <select class="form-select @error('kabupaten_id') is-invalid @enderror" id="add-kt-kabupaten" name="kabupaten_id" required @disabled(!old('provinsi_id'))>
<option value="">Pilih Kabupaten/Kota</option> <option value="">Pilih Kabupaten/Kota</option>
@foreach($kabupatenOptions ?? [] as $option)
<option value="{{ $option->id }}" @selected(old('kabupaten_id') == $option->id)>{{ $option->nama }}</option>
@endforeach
</select> </select>
@error('kabupaten_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="add-kt-kecamatan">Kecamatan<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-kecamatan">Kecamatan<span class="text-danger">*</span></label>
<select class="form-control @error('kecamatan_id') is-invalid @enderror" id="add-kt-kecamatan" name="kecamatan_id" disabled> <select class="form-select @error('kecamatan_id') is-invalid @enderror" id="add-kt-kecamatan" name="kecamatan_id" required @disabled(!old('kabupaten_id'))>
<option value="">Pilih Kecamatan</option> <option value="">Pilih Kecamatan</option>
@foreach($kecamatanOptions ?? [] as $option)
<option value="{{ $option->id }}" @selected(old('kecamatan_id') == $option->id)>{{ $option->nama }}</option>
@endforeach
</select> </select>
@error('kecamatan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="add-kt-desa">Desa/Kelurahan<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-desa">Desa/Kelurahan<span class="text-danger">*</span></label>
<select class="form-control @error('desa_kelurahan_id') is-invalid @enderror" id="add-kt-desa" name="desa_kelurahan_id" disabled> <select class="form-select @error('desa_kelurahan_id') is-invalid @enderror" id="add-kt-desa" name="desa_kelurahan_id" required @disabled(!old('kecamatan_id'))>
<option value="">Pilih Desa/Kelurahan</option> <option value="">Pilih Desa/Kelurahan</option>
@foreach($desaOptions ?? [] as $option)
<option value="{{ $option->id }}" @selected(old('desa_kelurahan_id') == $option->id)>{{ $option->nama }}</option>
@endforeach
</select> </select>
@error('desa_kelurahan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('desa_kelurahan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="add-kt-jenis-kelompok">Terdaftar/Dikenal Sebagai<span class="text-danger">*</span></label> <label class="form-label" for="add-kt-jenis-kelompok">Terdaftar/Dikenal Sebagai<span class="text-danger">*</span></label>
<select class="form-control @error('jenis_kelompok') is-invalid @enderror" id="add-kt-jenis-kelompok" name="jenis_kelompok"> <select class="form-select @error('jenis_kelompok') is-invalid @enderror" id="add-kt-jenis-kelompok" name="jenis_kelompok" required>
<option value="">Pilih Jenis Kelompok</option> <option value="">Pilih Jenis Kelompok</option>
<option value="Kelompok Tani (Poktan)" {{ old('jenis_kelompok') == 'Kelompok Tani (Poktan)' ? 'selected' : '' }}>Kelompok Tani (Poktan)</option> <option value="Kelompok Tani (Poktan)" {{ old('jenis_kelompok') == 'Kelompok Tani (Poktan)' ? 'selected' : '' }}>Kelompok Tani (Poktan)</option>
<option value="Kelompok Wanita Tani (KWT)" {{ old('jenis_kelompok') == 'Kelompok Wanita Tani (KWT)' ? 'selected' : '' }}>Kelompok Wanita Tani (KWT)</option> <option value="Kelompok Wanita Tani (KWT)" {{ old('jenis_kelompok') == 'Kelompok Wanita Tani (KWT)' ? 'selected' : '' }}>Kelompok Wanita Tani (KWT)</option>
@@ -111,7 +113,7 @@
@error('jenis_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('jenis_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-4 mb-4 form-control-validation" id="add-kt-jenis-kelompok-lainnya-wrapper" style="display: none;"> <div class="col-md-4 mb-4 form-control-validation" id="add-kt-jenis-kelompok-lainnya-wrapper" style="display: none;">
<label class="form-label" for="add-kt-jenis-kelompok-lainnya">Sebutkan Jenis Kelompok</label> <label class="form-label" for="add-kt-jenis-kelompok-lainnya">Sebutkan Jenis Kelompok<span class="text-danger">*</span></label>
<input type="text" class="form-control @error('jenis_kelompok_lainnya') is-invalid @enderror" id="add-kt-jenis-kelompok-lainnya" name="jenis_kelompok_lainnya" <input type="text" class="form-control @error('jenis_kelompok_lainnya') is-invalid @enderror" id="add-kt-jenis-kelompok-lainnya" name="jenis_kelompok_lainnya"
value="{{ old('jenis_kelompok_lainnya') }}" /> value="{{ old('jenis_kelompok_lainnya') }}" />
@error('jenis_kelompok_lainnya')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('jenis_kelompok_lainnya')<div class="error invalid-feedback">{{ $message }}</div>@enderror
@@ -136,9 +138,17 @@
</div> </div>
</div> </div>
@include('produktivitas.survey-kelompok-tani.pengurus-anggota', ['bagianKeanggotaan' => 'ringkasan', 'kelompokTani' => null])
<div class="mt-4 text-end"> <div class="mt-4 text-end">
<a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan & Lanjutkan</button> <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
Kembali
</a>
<button type="submit" class="btn btn-success data-submit wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
@@ -157,6 +167,7 @@
function toggleJenisKelompokFields() { function toggleJenisKelompokFields() {
var jenisKelompok = $('#add-kt-jenis-kelompok').val(); var jenisKelompok = $('#add-kt-jenis-kelompok').val();
$('#add-kt-jenis-kelompok-lainnya').prop('required', jenisKelompok === 'Lainnya').prop('disabled', jenisKelompok !== 'Lainnya');
if (jenisKelompok === 'Lainnya') { if (jenisKelompok === 'Lainnya') {
$('#add-kt-jenis-kelompok-lainnya-wrapper').show(); $('#add-kt-jenis-kelompok-lainnya-wrapper').show();
@@ -236,52 +247,6 @@
}); });
$('#add-kt-desa').select2(); $('#add-kt-desa').select2();
$('#addKelompokTaniForm').validate({
rules: {
nama_responden: { required: true },
nama_kelompok: { required: true },
provinsi_id: { required: true },
kabupaten_id: { required: true },
kecamatan_id: { required: true },
desa_kelurahan_id: { required: true },
jenis_kelompok: { required: true },
jenis_kelompok_lainnya: {
required: function () {
return $('#add-kt-jenis-kelompok').val() === 'Lainnya';
}
},
tahun_berdiri: { min: 1900, max: new Date().getFullYear() }
},
messages: {
nama_responden: { required: 'Nama responden wajib diisi' },
nama_kelompok: { required: 'Nama kelompok wajib diisi' },
provinsi_id: { required: 'Provinsi wajib diisi' },
kabupaten_id: { required: 'Kabupaten/Kota wajib diisi' },
kecamatan_id: { required: 'Kecamatan wajib diisi' },
desa_kelurahan_id: { required: 'Desa/Kelurahan wajib diisi' },
jenis_kelompok: { required: 'Jenis kelompok wajib dipilih' },
jenis_kelompok_lainnya: { required: 'Sebutkan jenis kelompoknya' }
},
errorElement: 'div',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
if (element.hasClass('select2-hidden-accessible')) {
element.closest('.form-control-validation').append(error);
} else {
element.closest('.mb-4, .form-control-validation').append(error);
}
},
highlight: function (element) {
$(element).addClass('is-invalid');
},
unhighlight: function (element) {
$(element).removeClass('is-invalid');
},
submitHandler: function (form) {
form.submit();
}
});
}); });
</script> </script>
@endpush @endpush
@@ -1,7 +1,7 @@
@extends('layout.main') @extends('layout.main')
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y kelompok-tani-page">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani]) @include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani])
@php @php
@@ -41,18 +41,39 @@
<div class="card mb-6" id="section-aktivitas-fungsi"> <div class="card mb-6" id="section-aktivitas-fungsi">
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [ <form novalidate method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
'section' => 'aktivitas-fungsi',
'errorFields' => ['kegiatan_terlaksana.*', 'kegiatan_terlaksana_lainnya', 'estimasi_panen_minimal', 'estimasi_panen_maksimal', 'satuan_hasil_panen', 'tindakan_hasil_panen.*', 'fungsi_kelompok_bagi_anggota.*', 'fungsi_kelompok_lainnya'],
])
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
@csrf @csrf
@method('PUT') @method('PUT')
<input type="hidden" name="section" value="aktivitas-fungsi"> <input type="hidden" name="section" value="aktivitas-fungsi">
<h5 class="pb-4 border-bottom mb-4">Kegiatan Konkret yang Sudah Berjalan</h5> <h5 class="pb-4 border-bottom mb-4">Frekuensi Pertemuan Rutin</h5>
<p class="text-muted" style="font-size: 0.8rem; margin-top: -0.75rem;"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="af-frekuensi-pertemuan">Frekuensi Pertemuan</label>
<select class="form-select @error('frekuensi_pertemuan') is-invalid @enderror" id="af-frekuensi-pertemuan" name="frekuensi_pertemuan">
<option value="">Pilih Frekuensi Pertemuan</option>
@foreach([
'Tidak pernah/sudah tidak aktif',
'Tidak rutin/tidak terjadwal',
'1x per 2-3 bulan',
'1x per bulan',
'Lebih dari 1x per bulan',
] as $option)
<option value="{{ $option }}" {{ old('frekuensi_pertemuan', $kelompokTani->frekuensi_pertemuan) == $option ? 'selected' : '' }}>{{ $option }}</option>
@endforeach
</select>
@error('frekuensi_pertemuan')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="af-tanggal-pertemuan-terakhir">Tanggal Pertemuan Terakhir</label>
<input type="text" class="form-control @error('tanggal_pertemuan_terakhir') is-invalid @enderror" id="af-tanggal-pertemuan-terakhir" name="tanggal_pertemuan_terakhir"
value="{{ old('tanggal_pertemuan_terakhir', $kelompokTani->tanggal_pertemuan_terakhir?->format('d-m-Y')) }}" />
@error('tanggal_pertemuan_terakhir')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Kegiatan Konkret yang Sudah Berjalan</h5>
<p class="text-muted small kelompok-tani-help">
Kegiatan yang sudah benar-benar dilaksanakan, bukan rencana. Boleh pilih lebih dari satu. Kegiatan yang sudah benar-benar dilaksanakan, bukan rencana. Boleh pilih lebih dari satu.
</p> </p>
<div class="row mb-2"> <div class="row mb-2">
@@ -99,7 +120,7 @@
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="af-satuan-panen">Satuan</label> <label class="form-label" for="af-satuan-panen">Satuan</label>
<select class="form-control @error('satuan_hasil_panen') is-invalid @enderror" id="af-satuan-panen" name="satuan_hasil_panen"> <select class="form-select @error('satuan_hasil_panen') is-invalid @enderror" id="af-satuan-panen" name="satuan_hasil_panen">
<option value="">Pilih Satuan</option> <option value="">Pilih Satuan</option>
<option value="kg" {{ old('satuan_hasil_panen', $kelompokTani->satuan_hasil_panen) == 'kg' ? 'selected' : '' }}>Kg</option> <option value="kg" {{ old('satuan_hasil_panen', $kelompokTani->satuan_hasil_panen) == 'kg' ? 'selected' : '' }}>Kg</option>
<option value="ton" {{ old('satuan_hasil_panen', $kelompokTani->satuan_hasil_panen) == 'ton' ? 'selected' : '' }}>Ton</option> <option value="ton" {{ old('satuan_hasil_panen', $kelompokTani->satuan_hasil_panen) == 'ton' ? 'selected' : '' }}>Ton</option>
@@ -123,7 +144,7 @@
</div> </div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Peran dan Fungsi Kelompok</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Peran dan Fungsi Kelompok</h5>
<p class="text-muted" style="font-size: 0.8rem; margin-top: -0.75rem;"> <p class="text-muted small kelompok-tani-help">
Fungsi yang melekat secara struktural pada kelompok ini, terlepas dari seberapa sering kegiatannya berjalan. Fungsi yang melekat secara struktural pada kelompok ini, terlepas dari seberapa sering kegiatannya berjalan.
</p> </p>
<div class="row mb-2"> <div class="row mb-2">
@@ -153,13 +174,20 @@
</div> </div>
<div class="mt-4 text-end"> <div class="mt-4 text-end">
<button type="submit" class="btn btn-success wave-effect wave-light"> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<i class="ti tabler-device-floppy me-2"></i> Simpan Aktivitas & Fungsi <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
</button> Kembali
</a>
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
@include('produktivitas.survey-kelompok-tani.aset-keuangan')
@include('produktivitas.survey-kelompok-tani.bantuan-penilaian')
</div> </div>
@endsection @endsection
@@ -167,6 +195,10 @@
<script> <script>
'use strict'; 'use strict';
$(document).ready(function () { $(document).ready(function () {
$('#af-frekuensi-pertemuan').select2();
$('#af-tanggal-pertemuan-terakhir').flatpickr({
dateFormat: 'd-m-Y'
});
function toggleLainnya(checkboxSelector, wrapperSelector, textInputSelector) { function toggleLainnya(checkboxSelector, wrapperSelector, textInputSelector) {
var $checkbox = $(checkboxSelector); var $checkbox = $(checkboxSelector);
@@ -1,9 +1,3 @@
@extends('layout.main')
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani])
@php @php
$jenisAsetBaku = \App\Http\Controllers\Web\Produktivitas\AsetKelompokTaniController::JENIS_ASET_BAKU; $jenisAsetBaku = \App\Http\Controllers\Web\Produktivitas\AsetKelompokTaniController::JENIS_ASET_BAKU;
$asetTersimpan = $kelompokTani->aset->keyBy('jenis_aset'); $asetTersimpan = $kelompokTani->aset->keyBy('jenis_aset');
@@ -11,19 +5,18 @@
$statusPengelolaanOptions = ['Swadaya', 'Hibah', 'Pinjaman', 'Sewa']; $statusPengelolaanOptions = ['Swadaya', 'Hibah', 'Pinjaman', 'Sewa'];
$kondisiOptions = ['Berfungsi', 'Perlu perbaikan/evaluasi', 'Rusak/tidak bisa dipakai', 'Mangkrak']; $kondisiOptions = ['Berfungsi', 'Perlu perbaikan/evaluasi', 'Rusak/tidak bisa dipakai', 'Mangkrak'];
$asetLainnyaBaru = old('aset_lainnya') ?: [[]];
$indexKas = array_search('Kas/simpanan kelompok', $jenisAsetBaku, true);
$memilikiKas = (bool) old("aset.$indexKas.status_aset", $asetTersimpan->get('Kas/simpanan kelompok')?->status_aset ?? false);
@endphp @endphp
{{-- ASET PRODUKTIF --}} {{-- ASET PRODUKTIF --}}
<div class="card mb-6" id="section-aset-baku"> <div class="card mb-6" id="section-aset-baku">
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [
'section' => 'aset-baku',
'errorFields' => ['aset.*'],
])
<h5 class="pb-4 border-bottom mb-4">Aset Produktif Milik Kelompok</h5> <h5 class="pb-4 border-bottom mb-4">Aset Produktif Milik Kelompok</h5>
<p class="text-muted small"><span class="text-danger">*</span> Wajib diisi untuk aset yang dipilih Ada.</p>
<form method="POST" action="{{ route('kelompok-tani.aset-keuangan.update', $kelompokTani->id) }}"> <form novalidate id="aset-produktif-form" method="POST" action="{{ route('kelompok-tani.aset-keuangan.update', $kelompokTani->id) }}">
@csrf @csrf
@method('PUT') @method('PUT')
@@ -33,43 +26,56 @@
<tr> <tr>
<th style="min-width: 180px;">Jenis Aset</th> <th style="min-width: 180px;">Jenis Aset</th>
<th style="width: 140px;">Ada/Tidak</th> <th style="width: 140px;">Ada/Tidak</th>
<th style="min-width: 180px;">Status Pengelolaan</th> <th style="min-width: 180px;">Status Pengelolaan <span class="text-danger" aria-hidden="true">*</span></th>
<th style="min-width: 200px;">Kondisi</th> <th style="min-width: 200px;">Kondisi <span class="text-danger" aria-hidden="true">*</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($jenisAsetBaku as $index => $jenis) @foreach($jenisAsetBaku as $index => $jenis)
@php $row = $asetTersimpan->get($jenis); @endphp @php
<tr> $row = $asetTersimpan->get($jenis);
$ada = (bool) old("aset.$index.status_aset", $row?->status_aset ?? false);
$statusPengelolaan = old("aset.$index.status_pengelolaan_aset", $row?->status_pengelolaan_aset);
$kondisi = old("aset.$index.kondisi_aset", $row?->kondisi_aset);
@endphp
<tr @if($index === $indexKas) data-aset-kas @endif>
<td> <td>
<input type="hidden" name="aset[{{ $index }}][jenis_aset]" value="{{ $jenis }}"> <input type="hidden" name="aset[{{ $index }}][jenis_aset]" value="{{ $jenis }}">
{{ $jenis }} {{ $jenis }}
</td> </td>
<td> <td>
<div class="form-check"> <div class="form-check">
<input class="form-check-input aset-status-radio" type="radio" name="aset[{{ $index }}][status_aset]" value="1" id="aset-{{ $index }}-ada" data-row="{{ $index }}" {{ $row?->status_aset ? 'checked' : '' }}> <input class="form-check-input aset-status-radio" type="radio" name="aset[{{ $index }}][status_aset]" value="1" id="aset-{{ $index }}-ada" data-row="{{ $index }}" @checked($ada)>
<label class="form-check-label" for="aset-{{ $index }}-ada">Ada</label> <label class="form-check-label" for="aset-{{ $index }}-ada">Ada</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input aset-status-radio" type="radio" name="aset[{{ $index }}][status_aset]" value="0" id="aset-{{ $index }}-tidak" data-row="{{ $index }}" {{ !$row || !$row->status_aset ? 'checked' : '' }}> <input class="form-check-input aset-status-radio" type="radio" name="aset[{{ $index }}][status_aset]" value="0" id="aset-{{ $index }}-tidak" data-row="{{ $index }}" @checked(!$ada)>
<label class="form-check-label" for="aset-{{ $index }}-tidak">Tidak</label> <label class="form-check-label" for="aset-{{ $index }}-tidak">Tidak</label>
</div> </div>
</td> </td>
<td> <td>
<select class="form-select form-select-sm aset-detail-select" name="aset[{{ $index }}][status_pengelolaan_aset]" {{ !$row?->status_aset ? 'disabled' : '' }}> <select class="form-select aset-detail-select @error("aset.$index.status_pengelolaan_aset") is-invalid @enderror" id="aset-{{ $index }}-status-pengelolaan" name="aset[{{ $index }}][status_pengelolaan_aset]" aria-label="Status Pengelolaan {{ $jenis }}" @disabled(!$ada) @required($ada)
<option value="">-</option> @error("aset.$index.status_pengelolaan_aset") aria-invalid="true" aria-describedby="aset-{{ $index }}-status-pengelolaan-error" @enderror>
<option value="">Pilih status pengelola</option>
@foreach($statusPengelolaanOptions as $option) @foreach($statusPengelolaanOptions as $option)
<option value="{{ $option }}" {{ $row?->status_pengelolaan_aset == $option ? 'selected' : '' }}>{{ $option }}</option> <option value="{{ $option }}" @selected($statusPengelolaan == $option)>{{ $option }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback" id="aset-{{ $index }}-status-pengelolaan-error" role="alert">
@error("aset.$index.status_pengelolaan_aset") {{ $message }} @else Status Pengelolaan wajib diisi jika aset dipilih Ada. @enderror
</div>
</td> </td>
<td> <td>
<select class="form-select form-select-sm aset-detail-select" name="aset[{{ $index }}][kondisi_aset]" {{ !$row?->status_aset ? 'disabled' : '' }}> <select class="form-select aset-detail-select @error("aset.$index.kondisi_aset") is-invalid @enderror" id="aset-{{ $index }}-kondisi" name="aset[{{ $index }}][kondisi_aset]" aria-label="Kondisi {{ $jenis }}" @disabled(!$ada) @required($ada)
<option value="">-</option> @error("aset.$index.kondisi_aset") aria-invalid="true" aria-describedby="aset-{{ $index }}-kondisi-error" @enderror>
<option value="">Pilih kondisi</option>
@foreach($kondisiOptions as $option) @foreach($kondisiOptions as $option)
<option value="{{ $option }}" {{ $row?->kondisi_aset == $option ? 'selected' : '' }}>{{ $option }}</option> <option value="{{ $option }}" @selected($kondisi == $option)>{{ $option }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback" id="aset-{{ $index }}-kondisi-error" role="alert">
@error("aset.$index.kondisi_aset") {{ $message }} @else Kondisi wajib diisi jika aset dipilih Ada. @enderror
</div>
</td> </td>
</tr> </tr>
@endforeach @endforeach
@@ -77,129 +83,121 @@
</table> </table>
</div> </div>
<div class="text-end"> <div id="section-aset-lainnya" class="mt-4">
<button type="submit" class="btn btn-success wave-effect wave-light"> <h6 class="pb-3 border-bottom mb-4">Aset Lainnya</h6>
<i class="ti tabler-device-floppy me-2"></i> Simpan Aset Produktif
</button>
</div>
</form>
</div>
</div>
{{-- ASET LAINNYA (di luar 8 jenis baku, bisa lebih dari satu) --}} {{-- DATA LAMA --}}
<div class="card mb-6" id="section-aset-lainnya"> @if($asetLainnyaList->isNotEmpty())
<div class="card-body pt-6"> @foreach($asetLainnyaList as $row)
@include('produktivitas.survey-kelompok-tani._alert', [ <div class="row align-items-end border-bottom pb-3 mb-3">
'section' => 'aset-lainnya', <div class="col-md-4">
'errorFields' => ['aset_lainnya.*'],
])
<h5 class="pb-4 border-bottom mb-4">Aset Lainnya</h5>
{{-- DATA LAMA --}}
@if($asetLainnyaList->isNotEmpty())
@foreach($asetLainnyaList as $row)
<div class="row align-items-end border-bottom pb-3 mb-3">
<div class="col-md-4">
<label class="form-label">Jenis Aset</label>
<input type="text" class="form-control" value="{{ $row->jenis_aset }}" disabled>
</div>
<div class="col-md-3">
<label class="form-label">Status Pengelolaan</label>
<input type="text" class="form-control" value="{{ $row->status_pengelolaan_aset }}" disabled>
</div>
<div class="col-md-4">
<label class="form-label">Kondisi</label>
<input type="text" class="form-control" value="{{ $row->kondisi_aset }}" disabled>
</div>
<div class="col-md-1">
<form method="POST" action="{{ route('kelompok-tani.aset-keuangan.destroy', [$kelompokTani->id, $row->id]) }}" class="delete-db-form">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-outline-danger">
<i class="icon-base ti tabler-trash icon-sm"></i>
</button>
</form>
</div>
</div>
@endforeach
@else
<p class="text-muted mb-4">Belum ada aset lainnya di luar daftar baku.</p>
@endif
{{-- TOMBOL PEMBUKA — form tambah disembunyikan dulu karena aset lainnya sifatnya opsional --}}
@php $bukaFormAsetLainnya = old('aset_lainnya') || $errors->has('aset_lainnya.*'); @endphp
<button type="button" class="btn btn-outline-success" id="btn-toggle-aset-lainnya" style="display: {{ $bukaFormAsetLainnya ? 'none' : 'inline-block' }};">
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambahkan Aset Lainnya
</button>
{{-- FORM TAMBAH DATA BARU — tetap kebuka kalau baru gagal validasi, biar isian & errornya kelihatan --}}
<div id="aset-lainnya-form-wrapper" style="display: {{ $bukaFormAsetLainnya ? 'block' : 'none' }};" class="mt-4">
<form method="POST" action="{{ route('kelompok-tani.aset-keuangan.store', $kelompokTani->id) }}" class="form-repeater-aset-lainnya">
@csrf
<div data-repeater-list="aset_lainnya">
<div data-repeater-item>
<div class="row align-items-end">
<div class="col-md-4 mb-3">
<label class="form-label">Jenis Aset</label> <label class="form-label">Jenis Aset</label>
<input type="text" class="form-control aset-lainnya-input" name="jenis_aset" placeholder="Cth: Mesin pengering"> <input type="text" class="form-control" value="{{ $row->jenis_aset }}" disabled>
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3">
<label class="form-label">Status Pengelolaan</label> <label class="form-label">Status Pengelolaan</label>
<select class="form-select aset-lainnya-select" name="status_pengelolaan_aset"> <input type="text" class="form-control" value="{{ $row->status_pengelolaan_aset }}" disabled>
<option value="">-</option>
@foreach($statusPengelolaanOptions as $option)
<option value="{{ $option }}">{{ $option }}</option>
@endforeach
</select>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4">
<label class="form-label">Kondisi</label> <label class="form-label">Kondisi</label>
<select class="form-select aset-lainnya-select" name="kondisi_aset"> <input type="text" class="form-control" value="{{ $row->kondisi_aset }}" disabled>
<option value="">-</option>
@foreach($kondisiOptions as $option)
<option value="{{ $option }}">{{ $option }}</option>
@endforeach
</select>
</div> </div>
<div class="col-md-1 mb-3"> <div class="col-md-1">
<button type="button" class="btn btn-outline-danger btn-delete-aset-lainnya" data-repeater-delete disabled> <button type="submit" form="delete-aset-{{ $row->id }}" class="btn btn-outline-danger" aria-label="Hapus aset {{ $row->jenis_aset }}">
<i class="icon-base ti tabler-trash icon-sm"></i> <i class="icon-base ti tabler-trash icon-sm"></i>
</button> </button>
</div> </div>
</div> </div>
<hr> @endforeach
@else
<p class="text-muted mb-4">Belum ada aset lainnya di luar daftar baku.</p>
@endif
{{-- TOMBOL PEMBUKA — form tambah disembunyikan dulu karena aset lainnya sifatnya opsional --}}
@php $bukaFormAsetLainnya = old('aset_lainnya') || $errors->has('aset_lainnya.*'); @endphp
<button type="button" class="btn btn-outline-success" id="btn-toggle-aset-lainnya" style="display: {{ $bukaFormAsetLainnya ? 'none' : 'inline-block' }};">
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambahkan Aset Lainnya
</button>
{{-- FORM TAMBAH DATA BARU — tetap kebuka kalau baru gagal validasi, biar isian & errornya kelihatan --}}
<div id="aset-lainnya-form-wrapper" style="display: {{ $bukaFormAsetLainnya ? 'block' : 'none' }};" class="mt-4">
<div class="form-repeater-aset-lainnya">
<div data-repeater-list="aset_lainnya">
@foreach($asetLainnyaBaru as $index => $item)
<div data-repeater-item>
<div class="row align-items-end">
<div class="col-md-4 mb-3">
<label class="form-label">Jenis Aset</label>
<input type="text" class="form-control aset-lainnya-input" name="jenis_aset" placeholder="Contoh: Mesin pengering" value="{{ $item['jenis_aset'] ?? '' }}">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Status Pengelolaan</label>
<select class="form-select aset-lainnya-select" name="status_pengelolaan_aset">
<option value="">Pilih status pengelola</option>
@foreach($statusPengelolaanOptions as $option)
<option value="{{ $option }}" @selected(($item['status_pengelolaan_aset'] ?? '') == $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Kondisi</label>
<select class="form-select aset-lainnya-select" name="kondisi_aset">
<option value="">Pilih kondisi</option>
@foreach($kondisiOptions as $option)
<option value="{{ $option }}" @selected(($item['kondisi_aset'] ?? '') == $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<div class="col-md-1 mb-3">
<button type="button" class="btn btn-outline-danger btn-delete-aset-lainnya" data-repeater-delete disabled>
<i class="icon-base ti tabler-trash icon-sm"></i>
</button>
</div>
</div>
<hr>
</div>
@endforeach
</div>
<div class="mt-4 d-flex flex-wrap justify-content-between gap-2">
<button type="button" class="btn btn-outline-success" id="btn-tambah-baris-aset-lainnya" data-repeater-create disabled>
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambah Baris
</button>
</div>
</div> </div>
</div> </div>
</div>
<div class="mt-4 d-flex justify-content-between"> <div class="mt-4 text-end">
<button type="button" class="btn btn-outline-success" id="btn-tambah-baris-aset-lainnya" data-repeater-create disabled> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambah Baris <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
</button> Kembali
<button type="submit" class="btn btn-success"> </a>
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i> Simpan Aset Lainnya <button type="submit" class="btn btn-success wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button> </button>
</div> </div>
</div>
</form>
@foreach($asetLainnyaList as $row)
<form id="delete-aset-{{ $row->id }}" method="POST" action="{{ route('kelompok-tani.aset-keuangan.destroy', [$kelompokTani->id, $row->id]) }}" class="delete-db-form" hidden>
@csrf
@method('DELETE')
</form> </form>
</div> @endforeach
</div> </div>
</div> </div>
{{-- KAS / SIMPAN-PINJAM --}} {{-- KAS / SIMPAN-PINJAM --}}
<div class="card mb-6" id="section-kas"> <div class="card mb-6" id="section-kas" @if(!$memilikiKas) hidden @endif>
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [
'section' => 'kas',
'errorFields' => ['pembukuan_kas', 'pengelola_kas', 'masalah_pengelolaan_kas_simpan_pinjam', 'deskripsi_masalah_pengelolaan_kas_simpan_pinjam'],
])
<h5 class="pb-4 border-bottom mb-4">Kapasitas Pengelolaan Kas/Simpan-Pinjam Kelompok</h5> <h5 class="pb-4 border-bottom mb-4">Kapasitas Pengelolaan Kas/Simpan-Pinjam Kelompok</h5>
<p class="text-muted" style="font-size: 0.8rem; margin-top: -0.75rem;"> <p class="text-muted small kelompok-tani-help">
Hanya isi bagian ini jika kelompok memiliki kas/simpan-pinjam (lihat baris "Kas/simpanan kelompok" di tabel aset). Hanya isi bagian ini jika kelompok memiliki kas/simpan-pinjam (lihat baris "Kas/simpanan kelompok" di tabel aset).
</p> </p>
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}"> <form novalidate method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
@csrf @csrf
@method('PUT') @method('PUT')
<input type="hidden" name="section" value="kas"> <input type="hidden" name="section" value="kas">
@@ -207,7 +205,7 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="ak-pembukuan-kas">Pembukuan/Pencatatan Keuangan Kas</label> <label class="form-label" for="ak-pembukuan-kas">Pembukuan/Pencatatan Keuangan Kas</label>
<select class="form-control @error('pembukuan_kas') is-invalid @enderror" id="ak-pembukuan-kas" name="pembukuan_kas"> <select class="form-select @error('pembukuan_kas') is-invalid @enderror" id="ak-pembukuan-kas" name="pembukuan_kas">
<option value="">Pilih Status Pembukuan</option> <option value="">Pilih Status Pembukuan</option>
@foreach([ @foreach([
'Tidak ada pencatatan sama sekali', 'Tidak ada pencatatan sama sekali',
@@ -227,7 +225,7 @@
</div> </div>
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="ak-masalah-kas">Pernah Ada Masalah Pengelolaan Kas?</label> <label class="form-label" for="ak-masalah-kas">Pernah Ada Masalah Pengelolaan Kas?</label>
<select class="form-control @error('masalah_pengelolaan_kas_simpan_pinjam') is-invalid @enderror" id="ak-masalah-kas" name="masalah_pengelolaan_kas_simpan_pinjam"> <select class="form-select @error('masalah_pengelolaan_kas_simpan_pinjam') is-invalid @enderror" id="ak-masalah-kas" name="masalah_pengelolaan_kas_simpan_pinjam">
<option value="">Pilih Status</option> <option value="">Pilih Status</option>
@foreach([ @foreach([
'Tidak pernah ada masalah', 'Tidak pernah ada masalah',
@@ -250,20 +248,51 @@
</div> </div>
<div class="text-end"> <div class="text-end">
<button type="submit" class="btn btn-success wave-effect wave-light"> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<i class="ti tabler-device-floppy me-2"></i> Simpan Kas/Simpan-Pinjam <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
</button> Kembali
</a>
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div>
@endsection
@push('scripts') @push('scripts')
<script> <script>
'use strict'; 'use strict';
$(document).ready(function () { $(document).ready(function () {
var assetForm = document.getElementById('aset-produktif-form');
assetForm.addEventListener('invalid', function (event) {
if (!event.target.matches('.aset-detail-select')) return;
event.preventDefault();
var field = event.target;
$(field).addClass('is-invalid').attr({
'aria-invalid': 'true',
'aria-describedby': field.id + '-error'
});
if (field === assetForm.querySelector('.aset-detail-select:invalid')) {
field.focus();
}
}, true);
$('.aset-detail-select').on('change', function () {
if (this.validity.valid) {
$(this).removeClass('is-invalid').removeAttr('aria-invalid aria-describedby');
}
});
function toggleKasSection() {
var memilikiKas = $('[data-aset-kas] .aset-status-radio[value="1"]').is(':checked');
$('#section-kas').prop('hidden', !memilikiKas);
}
toggleKasSection();
$('[data-aset-kas] .aset-status-radio').on('change', toggleKasSection);
function toggleDeskripsiMasalah() { function toggleDeskripsiMasalah() {
var val = $('#ak-masalah-kas').val(); var val = $('#ak-masalah-kas').val();
$('#ak-deskripsi-masalah-wrapper').toggle(val === 'Pernah, sudah selesai' || val === 'Pernah, masih berlanjut/belum selesai'); $('#ak-deskripsi-masalah-wrapper').toggle(val === 'Pernah, sudah selesai' || val === 'Pernah, masih berlanjut/belum selesai');
@@ -277,9 +306,9 @@
var $row = $(this).closest('tr'); var $row = $(this).closest('tr');
var ada = $(this).val() === '1'; var ada = $(this).val() === '1';
$row.find('.aset-detail-select').prop('disabled', !ada); $row.find('.aset-detail-select').prop({ disabled: !ada, required: ada });
if (!ada) { if (!ada) {
$row.find('.aset-detail-select').val(''); $row.find('.aset-detail-select').val('').removeClass('is-invalid').removeAttr('aria-invalid aria-describedby');
} }
}); });
@@ -298,7 +327,7 @@
} }
function rowAdaIsi($row) { function rowAdaIsi($row) {
return String($row.find('input[name="jenis_aset"]').val() || '').trim() !== ''; return String($row.find('.aset-lainnya-input').val() || '').trim() !== '';
} }
function updateButtons() { function updateButtons() {
@@ -346,10 +375,10 @@
} }
}); });
$(document).on('input keyup change', '.form-repeater-aset-lainnya input[name="jenis_aset"]', updateButtons); $(document).on('input keyup change', '.form-repeater-aset-lainnya .aset-lainnya-input', updateButtons);
updateButtons(); updateButtons();
$(document).on('submit', '.delete-db-form', function (e) { $(document).on('submit', '#section-aset-baku .delete-db-form', function (e) {
e.preventDefault(); e.preventDefault();
var form = this; var form = this;
@@ -1,253 +1,190 @@
@extends('layout.main') @php
$pernahBantuan = old('pernah_mendapatkan_bantuan', $kelompokTani->pernah_mendapatkan_bantuan);
$tampilkanBantuan = (bool) $pernahBantuan;
$hasSavedAid = $kelompokTani->bantuan->contains(fn ($bantuan) => filled($bantuan->nama_program));
$requireFirstRow = $tampilkanBantuan && !$hasSavedAid;
$bantuanBaru = old('bantuan') ?: [[]];
$bantuanBaru = is_array($bantuanBaru) ? $bantuanBaru : [[]];
@endphp
@section('content') <div class="card mb-6" id="section-status-bantuan">
<div class="container-xxl flex-grow-1 container-p-y"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani]) <h5 class="pb-4 border-bottom mb-4">Pengalaman Akses Bantuan/Program</h5>
{{-- STATUS AKSES BANTUAN --}} <form novalidate id="pengalaman-bantuan-form" method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
<div class="card mb-6" id="section-status-bantuan"> @csrf
<div class="card-body pt-6"> @method('PUT')
@include('produktivitas.survey-kelompok-tani._alert', [ <input type="hidden" name="section" value="status-bantuan">
'section' => 'status-bantuan',
'errorFields' => ['pernah_mendapatkan_bantuan'],
])
<h5 class="pb-4 border-bottom mb-4">Pengalaman Akses Bantuan/Program</h5> <div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}"> <label class="form-label d-block">Apakah kelompok ini pernah mendapatkan bantuan atau akses program dari pemerintah/lembaga lain?<span class="text-danger">*</span></label>
@csrf <div class="form-check form-check-inline">
@method('PUT') <input class="form-check-input @error('pernah_mendapatkan_bantuan') is-invalid @enderror" type="radio" name="pernah_mendapatkan_bantuan" value="1" id="sb-pernah" @checked($tampilkanBantuan) required>
<input type="hidden" name="section" value="status-bantuan"> <label class="form-check-label" for="sb-pernah">Pernah</label>
<div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block">Apakah kelompok ini pernah mendapatkan bantuan atau akses program dari pemerintah/lembaga lain?</label>
@php $pernahBantuan = old('pernah_mendapatkan_bantuan', $kelompokTani->pernah_mendapatkan_bantuan); @endphp
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="pernah_mendapatkan_bantuan" value="1" id="sb-pernah" {{ $pernahBantuan ? 'checked' : '' }}>
<label class="form-check-label" for="sb-pernah">Pernah</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="pernah_mendapatkan_bantuan" value="0" id="sb-belum" {{ !is_null($pernahBantuan) && !$pernahBantuan ? 'checked' : '' }}>
<label class="form-check-label" for="sb-belum">Belum Pernah</label>
</div>
@error('pernah_mendapatkan_bantuan')<div class="error invalid-feedback d-block">{{ $message }}</div>@enderror
</div> </div>
<div class="form-check form-check-inline">
<input class="form-check-input @error('pernah_mendapatkan_bantuan') is-invalid @enderror" type="radio" name="pernah_mendapatkan_bantuan" value="0" id="sb-belum" @checked(!is_null($pernahBantuan) && !$tampilkanBantuan) required>
<label class="form-check-label" for="sb-belum">Belum Pernah</label>
</div>
@error('pernah_mendapatkan_bantuan')<div class="error invalid-feedback d-block">{{ $message }}</div>@enderror
</div> </div>
</div>
<div class="text-end"> <div id="section-bantuan" @if(!$tampilkanBantuan) hidden @endif>
<button type="submit" class="btn btn-success wave-effect wave-light"> <h6 class="pb-3 border-bottom mb-4">Daftar Program/Bantuan yang Pernah Diterima</h6>
<i class="ti tabler-device-floppy me-2"></i> Simpan Status <p class="text-muted small mb-4">Minimal satu data bantuan wajib diisi.</p>
</button>
</div>
</form>
</div>
</div>
{{-- DAFTAR PROGRAM / BANTUAN --}} @forelse($kelompokTani->bantuan as $row)
<div class="card mb-6" id="section-bantuan">
<div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [
'section' => 'bantuan',
'errorFields' => ['bantuan.*'],
])
<h5 class="pb-4 border-bottom mb-4">Daftar Program/Bantuan yang Pernah Diterima</h5>
<p class="text-muted mb-0" id="bantuan-list-placeholder" style="display: none;">
Set status di atas jadi <strong>"Pernah"</strong> dulu untuk mengisi daftar program/bantuan.
</p>
<div id="bantuan-list-content">
{{-- DATA LAMA --}}
@if($kelompokTani->bantuan->count() > 0)
@foreach($kelompokTani->bantuan as $row)
<div class="row align-items-end border-bottom pb-3 mb-3"> <div class="row align-items-end border-bottom pb-3 mb-3">
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Nama Program/Bantuan</label> <label class="form-label">Nama Program/Bantuan @if($loop->first)<span class="text-danger bantuan-saved-required-mark" aria-hidden="true">*</span>@endif</label>
<input type="text" class="form-control" value="{{ $row->nama_program }}" disabled> <input type="text" class="form-control" value="{{ $row->nama_program }}" disabled>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Penyelenggara</label> <label class="form-label">Penyelenggara @if($loop->first)<span class="text-danger bantuan-saved-required-mark" aria-hidden="true">*</span>@endif</label>
<input type="text" class="form-control" value="{{ $row->penyelenggara }}" disabled> <input type="text" class="form-control" value="{{ $row->penyelenggara }}" disabled>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Bentuk Bantuan</label> <label class="form-label">Bentuk Bantuan @if($loop->first)<span class="text-danger bantuan-saved-required-mark" aria-hidden="true">*</span>@endif</label>
<input type="text" class="form-control" value="{{ $row->bentuk_bantuan }}" disabled> <input type="text" class="form-control" value="{{ $row->bentuk_bantuan }}" disabled>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">Tahun</label> <label class="form-label">Tahun @if($loop->first)<span class="text-danger bantuan-saved-required-mark" aria-hidden="true">*</span>@endif</label>
<input type="text" class="form-control" value="{{ $row->tahun }}" disabled> <input type="text" class="form-control" value="{{ $row->tahun }}" disabled>
</div> </div>
<div class="col-md-1"> <div class="col-md-1">
<form method="POST" action="{{ route('kelompok-tani.bantuan-penilaian.destroy', [$kelompokTani->id, $row->id]) }}" class="delete-db-form"> <button type="submit" form="delete-bantuan-{{ $row->id }}" class="btn btn-outline-danger" aria-label="Hapus bantuan {{ $row->nama_program }}">
@csrf <i class="icon-base ti tabler-trash icon-sm"></i>
@method('DELETE') </button>
<button type="submit" class="btn btn-outline-danger">
<i class="icon-base ti tabler-trash icon-sm"></i>
</button>
</form>
</div> </div>
</div> </div>
@endforeach @empty
@else <p class="text-muted mb-4">Belum ada data program/bantuan.</p>
<p class="text-muted mb-4">Belum ada data program/bantuan.</p> @endforelse
@endif
{{-- FORM TAMBAH DATA BARU --}} <fieldset id="bantuan-fields" @disabled(!$tampilkanBantuan)>
<form method="POST" action="{{ route('kelompok-tani.bantuan-penilaian.store', $kelompokTani->id) }}" class="form-repeater-bantuan"> <div class="form-repeater-bantuan" data-has-saved-bantuan="{{ $hasSavedAid ? '1' : '0' }}">
@csrf <div data-repeater-list="bantuan">
@foreach($bantuanBaru as $index => $item)
<div data-repeater-list="bantuan"> @php
<div data-repeater-item> $item = is_array($item) ? $item : [];
<div class="row align-items-end"> $required = $loop->first && $requireFirstRow;
<div class="col-md-3 mb-3"> @endphp
<label class="form-label">Nama Program/Bantuan</label> <div data-repeater-item>
<input type="text" class="form-control bantuan-input" name="nama_program" placeholder="Cth: Bantuan Bibit Kakao"> <div class="row align-items-end">
</div> <div class="col-md-3 mb-3">
<div class="col-md-3 mb-3"> <label class="form-label">Nama Program/Bantuan <span class="text-danger bantuan-required-mark" @if(!$required) hidden @endif>*</span></label>
<label class="form-label">Penyelenggara</label> <input type="text" class="form-control bantuan-input @error("bantuan.$index.nama_program") is-invalid @enderror" name="nama_program" placeholder="Contoh: Bantuan Bibit Kakao" value="{{ $item['nama_program'] ?? '' }}" @required($required)>
<input type="text" class="form-control bantuan-input" name="penyelenggara" placeholder="Cth: Dinas Pertanian"> @error("bantuan.$index.nama_program")<div class="invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label">Bentuk Bantuan</label> <label class="form-label">Penyelenggara <span class="text-danger bantuan-required-mark" @if(!$required) hidden @endif>*</span></label>
<input type="text" class="form-control bantuan-input" name="bentuk_bantuan" placeholder="Cth: Bibit, pupuk, pelatihan"> <input type="text" class="form-control bantuan-input @error("bantuan.$index.penyelenggara") is-invalid @enderror" name="penyelenggara" placeholder="Contoh: Dinas Pertanian" value="{{ $item['penyelenggara'] ?? '' }}" @required($required)>
</div> @error("bantuan.$index.penyelenggara")<div class="invalid-feedback">{{ $message }}</div>@enderror
<div class="col-md-2 mb-3"> </div>
<label class="form-label">Tahun</label> <div class="col-md-3 mb-3">
<input type="number" class="form-control" name="tahun" placeholder="Cth: 2023" min="1900" max="{{ date('Y') }}"> <label class="form-label">Bentuk Bantuan <span class="text-danger bantuan-required-mark" @if(!$required) hidden @endif>*</span></label>
</div> <input type="text" class="form-control bantuan-input @error("bantuan.$index.bentuk_bantuan") is-invalid @enderror" name="bentuk_bantuan" placeholder="Contoh: Bibit, pupuk, pelatihan" value="{{ $item['bentuk_bantuan'] ?? '' }}" @required($required)>
<div class="col-md-1 mb-3"> @error("bantuan.$index.bentuk_bantuan")<div class="invalid-feedback">{{ $message }}</div>@enderror
<button type="button" class="btn btn-outline-danger btn-delete-bantuan" data-repeater-delete disabled> </div>
<i class="icon-base ti tabler-trash icon-sm"></i> <div class="col-md-2 mb-3">
</button> <label class="form-label">Tahun <span class="text-danger bantuan-required-mark" @if(!$required) hidden @endif>*</span></label>
</div> <input type="number" class="form-control bantuan-year-input @error("bantuan.$index.tahun") is-invalid @enderror" name="tahun" placeholder="Contoh: 2023" min="1900" max="{{ date('Y') }}" value="{{ $item['tahun'] ?? '' }}" @required($required)>
@error("bantuan.$index.tahun")<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-1 mb-3">
<button type="button" class="btn btn-outline-danger btn-delete-bantuan" data-repeater-delete disabled>
<i class="icon-base ti tabler-trash icon-sm"></i>
</button>
</div>
</div>
<hr>
</div>
@endforeach
</div> </div>
<hr>
</div>
</div>
<div class="mt-4 d-flex justify-content-between"> <button type="button" class="btn btn-outline-success" id="btn-tambah-bantuan" data-repeater-create disabled>
<button type="button" class="btn btn-outline-success" id="btn-tambah-bantuan" data-repeater-create disabled> <i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambah Baris
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambah Baris </button>
</button> </div>
<button type="submit" class="btn btn-success"> </fieldset>
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i> Simpan Daftar Bantuan
</button>
</div>
</form>
</div> </div>
</div>
</div>
{{-- POTENSI MITRA AGREGASI --}} <div class="mt-4 text-end">
<div class="card mb-6" id="section-penilaian"> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<div class="card-body pt-6"> <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
@include('produktivitas.survey-kelompok-tani._alert', [ Kembali
'section' => 'penilaian', </a>
'errorFields' => ['potensi_kelompok', 'alasan_penilaian_potensi'], <button type="submit" class="btn btn-success wave-effect wave-light">
]) <i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
<h5 class="pb-4 border-bottom mb-4">Potensi sebagai Mitra Agregasi/Pengolahan Kolektif</h5>
<p class="text-muted" style="font-size: 0.8rem; margin-top: -0.75rem;">
Ini penilaian enumerator, bukan pernyataan langsung dari responden — dasarkan pada kombinasi kegiatan, fungsi, dan aset yang sudah tergali di tab-tab sebelumnya.
</p>
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
@csrf
@method('PUT')
<input type="hidden" name="section" value="penilaian">
<div class="row">
<div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="pn-potensi-kelompok">Tingkat Potensi</label>
<select class="form-control @error('potensi_kelompok') is-invalid @enderror" id="pn-potensi-kelompok" name="potensi_kelompok">
<option value="">Pilih Tingkat Potensi</option>
<option value="Rendah" {{ old('potensi_kelompok', $kelompokTani->potensi_kelompok) == 'Rendah' ? 'selected' : '' }}>Rendah — belum ada kapasitas/aktivitas yang mendukung</option>
<option value="Sedang" {{ old('potensi_kelompok', $kelompokTani->potensi_kelompok) == 'Sedang' ? 'selected' : '' }}>Sedang — ada sebagian kapasitas, perlu penguatan lebih jauh</option>
<option value="Tinggi" {{ old('potensi_kelompok', $kelompokTani->potensi_kelompok) == 'Tinggi' ? 'selected' : '' }}>Tinggi — kapasitas dan aktivitas sudah mendukung peran ini</option>
</select>
@error('potensi_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-8 mb-4 form-control-validation">
<label class="form-label" for="pn-alasan-penilaian">Alasan Penilaian</label>
<textarea class="form-control @error('alasan_penilaian_potensi') is-invalid @enderror" id="pn-alasan-penilaian" name="alasan_penilaian_potensi" rows="2">{{ old('alasan_penilaian_potensi', $kelompokTani->alasan_penilaian_potensi) }}</textarea>
@error('alasan_penilaian_potensi')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<div class="text-end">
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="ti tabler-device-floppy me-2"></i> Simpan Penilaian
</button> </button>
</div> </div>
</div>
</form>
@foreach($kelompokTani->bantuan as $row)
<form id="delete-bantuan-{{ $row->id }}" method="POST" action="{{ route('kelompok-tani.bantuan-penilaian.destroy', [$kelompokTani->id, $row->id]) }}" class="delete-db-form" hidden>
@csrf
@method('DELETE')
</form> </form>
</div> @endforeach
</div> </div>
</div> </div>
@endsection
@push('scripts') @push('scripts')
<script> <script>
'use strict'; 'use strict';
$(function () { $(function () {
$('#pn-potensi-kelompok').select2();
// Daftar bantuan cuma relevan kalau status-nya "Pernah" — biar nggak kontradiktif
// (jawab "Belum Pernah" tapi daftar bantuannya keisi).
function toggleDaftarBantuan() { function toggleDaftarBantuan() {
var pernah = $('#sb-pernah').is(':checked'); var pernah = $('#sb-pernah').is(':checked');
$('#bantuan-list-content').toggle(pernah); $('#section-bantuan').prop('hidden', !pernah);
$('#bantuan-list-placeholder').toggle(!pernah); $('#bantuan-fields').prop('disabled', !pernah);
updateButtons();
} }
toggleDaftarBantuan();
$('#sb-pernah, #sb-belum').on('change', toggleDaftarBantuan);
var $form = $('.form-repeater-bantuan'); var $repeater = $('.form-repeater-bantuan');
var $btnTambah = $('#btn-tambah-bantuan'); var $btnTambah = $('#btn-tambah-bantuan');
function getRows() {
return $form.find('[data-repeater-item]');
}
function rowAdaIsi($row) { function rowAdaIsi($row) {
return $row.find('.bantuan-input').toArray().some(function (input) { return $row.find('.bantuan-input, .bantuan-year-input').toArray().some(function (input) {
return String($(input).val() || '').trim() !== ''; return String($(input).val() || '').trim() !== '';
}); });
} }
function rowLengkap($row) { function rowLengkap($row) {
var $inputs = $row.find('.bantuan-input'); return $row.find('.bantuan-input, .bantuan-year-input[required]').toArray().every(function (input) {
return $inputs.toArray().every(function (input) {
return String($(input).val() || '').trim() !== ''; return String($(input).val() || '').trim() !== '';
}); });
} }
function updateButtons() { function updateButtons() {
var $rows = getRows(); var $rows = $repeater.find('[data-repeater-item]');
var $lastRow = $rows.last(); var needsFirst = $('#sb-pernah').is(':checked') && $repeater.attr('data-has-saved-bantuan') !== '1';
$rows.each(function (index) {
$rows.each(function () { var required = needsFirst && index === 0;
var $row = $(this); $(this).find('.bantuan-input, .bantuan-year-input').prop('required', required);
$row.find('.btn-delete-bantuan').prop('disabled', !rowAdaIsi($row)); $(this).find('.bantuan-required-mark').prop('hidden', !required);
$(this).find('.btn-delete-bantuan').prop('disabled', $rows.length === 1 && (required || !rowAdaIsi($(this))));
}); });
$btnTambah.prop('disabled', !rowLengkap($rows.last()));
$btnTambah.prop('disabled', !rowLengkap($lastRow));
} }
$form.repeater({ $repeater.repeater({
initEmpty: false, initEmpty: false,
show: function () { show: function () {
var $row = $(this); $(this).find('.bantuan-input, .bantuan-year-input').val('');
$row.find('.bantuan-input, input[name="tahun"]').val(''); $(this).find('.is-invalid').removeClass('is-invalid');
$row.find('.btn-delete-bantuan').prop('disabled', true); $(this).find('.invalid-feedback').remove();
$row.slideDown(); $(this).slideDown();
setTimeout(updateButtons, 50); updateButtons();
}, },
hide: function (deleteElement) { hide: function (deleteElement) {
var $row = $(this); var $row = $(this);
if (!rowAdaIsi($row)) { if (!rowAdaIsi($row)) {
deleteElement();
updateButtons();
return; return;
} }
Swal.fire({ Swal.fire({
@@ -261,22 +198,20 @@ $(function () {
buttonsStyling: false buttonsStyling: false
}).then(function (result) { }).then(function (result) {
if (result.isConfirmed) { if (result.isConfirmed) {
$row.slideUp(deleteElement); $row.slideUp(function () { deleteElement(); updateButtons(); });
setTimeout(updateButtons, 300);
} }
}); });
} }
}); });
$(document).on('input change', '.form-repeater-bantuan .bantuan-input, .form-repeater-bantuan .bantuan-year-input', updateButtons);
toggleDaftarBantuan();
$('#sb-pernah, #sb-belum').on('change', toggleDaftarBantuan);
$(document).on('input keyup change', '.form-repeater-bantuan .bantuan-input', updateButtons); $(document).on('submit', '#section-status-bantuan .delete-db-form', function (event) {
updateButtons(); event.preventDefault();
$(document).on('submit', '.delete-db-form', function (e) {
e.preventDefault();
var form = this; var form = this;
Swal.fire({ Swal.fire({
title: 'Hapus data ini?', title: 'Hapus data bantuan ini?',
text: 'Data yang sudah dihapus tidak bisa dikembalikan.', text: 'Data yang sudah dihapus tidak bisa dikembalikan.',
icon: 'warning', icon: 'warning',
showCancelButton: true, showCancelButton: true,
@@ -285,9 +220,7 @@ $(function () {
customClass: { confirmButton: 'btn btn-danger me-3', cancelButton: 'btn btn-label-secondary' }, customClass: { confirmButton: 'btn btn-danger me-3', cancelButton: 'btn btn-label-secondary' },
buttonsStyling: false buttonsStyling: false
}).then(function (result) { }).then(function (result) {
if (result.isConfirmed) { if (result.isConfirmed) form.submit();
form.submit();
}
}); });
}); });
}); });
@@ -1,19 +1,14 @@
@extends('layout.main') @extends('layout.main')
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y kelompok-tani-page">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani]) @include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani])
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card mb-6" id="section-identitas"> <div class="card mb-6" id="section-identitas">
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [ <form novalidate id="editKelompokTaniForm" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}" method="POST">
'section' => 'identitas',
'errorFields' => ['nama_responden', 'jabatan_responden', 'kontak_responden', 'nama_kelompok', 'desa_kelurahan_id', 'dusun', 'tahun_berdiri', 'jenis_kelompok', 'jenis_kelompok_lainnya', 'nama_gapoktan', 'komoditas_id.*'],
])
<form id="editKelompokTaniForm" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}" method="POST">
@csrf @csrf
@method('PUT') @method('PUT')
@@ -21,7 +16,7 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-nama-responden">Nama Responden<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-nama-responden">Nama Responden<span class="text-danger">*</span></label>
<input type="text" class="form-control @error('nama_responden') is-invalid @enderror" id="edit-kt-nama-responden" name="nama_responden" <input type="text" class="form-control @error('nama_responden') is-invalid @enderror" id="edit-kt-nama-responden" name="nama_responden" required
placeholder="Cth: John Doe" value="{{ old('nama_responden', $kelompokTani->nama_responden) }}" /> placeholder="Cth: John Doe" value="{{ old('nama_responden', $kelompokTani->nama_responden) }}" />
@error('nama_responden')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('nama_responden')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
@@ -43,7 +38,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-nama-kelompok">Nama Kelompok Tani<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-nama-kelompok">Nama Kelompok Tani<span class="text-danger">*</span></label>
<input type="text" class="form-control @error('nama_kelompok') is-invalid @enderror" id="edit-kt-nama-kelompok" name="nama_kelompok" <input type="text" class="form-control @error('nama_kelompok') is-invalid @enderror" id="edit-kt-nama-kelompok" name="nama_kelompok" required
placeholder="Cth: Kelompok Tani Maju Bersama" value="{{ old('nama_kelompok', $kelompokTani->nama_kelompok) }}" /> placeholder="Cth: Kelompok Tani Maju Bersama" value="{{ old('nama_kelompok', $kelompokTani->nama_kelompok) }}" />
@error('nama_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('nama_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
@@ -64,45 +59,48 @@
<div class="row"> <div class="row">
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-provinsi">Provinsi<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-provinsi">Provinsi<span class="text-danger">*</span></label>
<select class="form-control @error('provinsi_id') is-invalid @enderror" id="edit-kt-provinsi" name="provinsi_id"> <select class="form-select @error('provinsi_id') is-invalid @enderror" id="edit-kt-provinsi" name="provinsi_id" required>
<option value="">Pilih Provinsi</option> <option value="">Pilih Provinsi</option>
@foreach($provinsi as $p) @foreach($provinsi as $p)
<option value="{{ $p->id }}" {{ old('provinsi_id', $kelompokTani->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id) == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option> <option value="{{ $p->id }}" {{ old('provinsi_id', $kelompokTani->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id) == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option>
@endforeach @endforeach
</select> </select>
@error('provinsi_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-kabupaten">Kabupaten/Kota<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-kabupaten">Kabupaten/Kota<span class="text-danger">*</span></label>
<select class="form-control @error('kabupaten_id') is-invalid @enderror" id="edit-kt-kabupaten" name="kabupaten_id"> <select class="form-select @error('kabupaten_id') is-invalid @enderror" id="edit-kt-kabupaten" name="kabupaten_id" required>
<option value="">Pilih Kabupaten/Kota</option> <option value="">Pilih Kabupaten/Kota</option>
@if($kelompokTani->desaKelurahan?->kecamatan?->kabupatenKota) @foreach($kabupatenOptions ?? [] as $option)
<option value="{{ $kelompokTani->desaKelurahan->kecamatan->kabupatenKota->id }}" selected>{{ $kelompokTani->desaKelurahan->kecamatan->kabupatenKota->nama }}</option> <option value="{{ $option->id }}" @selected(old('kabupaten_id', $kelompokTani->desaKelurahan?->kecamatan?->kabupaten_kota_id) == $option->id)>{{ $option->nama }}</option>
@endif @endforeach
</select> </select>
@error('kabupaten_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-kecamatan">Kecamatan<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-kecamatan">Kecamatan<span class="text-danger">*</span></label>
<select class="form-control @error('kecamatan_id') is-invalid @enderror" id="edit-kt-kecamatan" name="kecamatan_id"> <select class="form-select @error('kecamatan_id') is-invalid @enderror" id="edit-kt-kecamatan" name="kecamatan_id" required>
<option value="">Pilih Kecamatan</option> <option value="">Pilih Kecamatan</option>
@if($kelompokTani->desaKelurahan?->kecamatan) @foreach($kecamatanOptions ?? [] as $option)
<option value="{{ $kelompokTani->desaKelurahan->kecamatan->id }}" selected>{{ $kelompokTani->desaKelurahan->kecamatan->nama }}</option> <option value="{{ $option->id }}" @selected(old('kecamatan_id', $kelompokTani->desaKelurahan?->kecamatan_id) == $option->id)>{{ $option->nama }}</option>
@endif @endforeach
</select> </select>
@error('kecamatan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-desa">Desa/Kelurahan<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-desa">Desa/Kelurahan<span class="text-danger">*</span></label>
<select class="form-control @error('desa_kelurahan_id') is-invalid @enderror" id="edit-kt-desa" name="desa_kelurahan_id"> <select class="form-select @error('desa_kelurahan_id') is-invalid @enderror" id="edit-kt-desa" name="desa_kelurahan_id" required>
<option value="">Pilih Desa/Kelurahan</option> <option value="">Pilih Desa/Kelurahan</option>
@if($kelompokTani->desaKelurahan) @foreach($desaOptions ?? [] as $option)
<option value="{{ $kelompokTani->desaKelurahan->id }}" selected>{{ $kelompokTani->desaKelurahan->nama }}</option> <option value="{{ $option->id }}" @selected(old('desa_kelurahan_id', $kelompokTani->desa_kelurahan_id) == $option->id)>{{ $option->nama }}</option>
@endif @endforeach
</select> </select>
@error('desa_kelurahan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('desa_kelurahan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="edit-kt-jenis-kelompok">Terdaftar/Dikenal Sebagai<span class="text-danger">*</span></label> <label class="form-label" for="edit-kt-jenis-kelompok">Terdaftar/Dikenal Sebagai<span class="text-danger">*</span></label>
@php @php
$jenisKelompokOptions = [ $jenisKelompokOptions = [
@@ -115,7 +113,7 @@
$currentJenisKelompok = old('jenis_kelompok', $kelompokTani->jenis_kelompok); $currentJenisKelompok = old('jenis_kelompok', $kelompokTani->jenis_kelompok);
$isJenisKelompokLainnya = $currentJenisKelompok && !in_array($currentJenisKelompok, $jenisKelompokOptions); $isJenisKelompokLainnya = $currentJenisKelompok && !in_array($currentJenisKelompok, $jenisKelompokOptions);
@endphp @endphp
<select class="form-control @error('jenis_kelompok') is-invalid @enderror" id="edit-kt-jenis-kelompok" name="jenis_kelompok"> <select class="form-select @error('jenis_kelompok') is-invalid @enderror" id="edit-kt-jenis-kelompok" name="jenis_kelompok" required>
<option value="">Pilih Jenis Kelompok</option> <option value="">Pilih Jenis Kelompok</option>
@foreach($jenisKelompokOptions as $option) @foreach($jenisKelompokOptions as $option)
<option value="{{ $option }}" {{ $currentJenisKelompok == $option ? 'selected' : '' }}>{{ $option }}</option> <option value="{{ $option }}" {{ $currentJenisKelompok == $option ? 'selected' : '' }}>{{ $option }}</option>
@@ -125,7 +123,7 @@
@error('jenis_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('jenis_kelompok')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-4 mb-4 form-control-validation" id="edit-kt-jenis-kelompok-lainnya-wrapper" style="display: none;"> <div class="col-md-4 mb-4 form-control-validation" id="edit-kt-jenis-kelompok-lainnya-wrapper" style="display: none;">
<label class="form-label" for="edit-kt-jenis-kelompok-lainnya">Sebutkan Jenis Kelompok</label> <label class="form-label" for="edit-kt-jenis-kelompok-lainnya">Sebutkan Jenis Kelompok<span class="text-danger">*</span></label>
<input type="text" class="form-control @error('jenis_kelompok_lainnya') is-invalid @enderror" id="edit-kt-jenis-kelompok-lainnya" name="jenis_kelompok_lainnya" <input type="text" class="form-control @error('jenis_kelompok_lainnya') is-invalid @enderror" id="edit-kt-jenis-kelompok-lainnya" name="jenis_kelompok_lainnya"
value="{{ old('jenis_kelompok_lainnya', $isJenisKelompokLainnya ? $currentJenisKelompok : null) }}" /> value="{{ old('jenis_kelompok_lainnya', $isJenisKelompokLainnya ? $currentJenisKelompok : null) }}" />
@error('jenis_kelompok_lainnya')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('jenis_kelompok_lainnya')<div class="error invalid-feedback">{{ $message }}</div>@enderror
@@ -150,15 +148,24 @@
</div> </div>
</div> </div>
@include('produktivitas.survey-kelompok-tani.pengurus-anggota', ['bagianKeanggotaan' => 'ringkasan'])
<div class="mt-4 text-end"> <div class="mt-4 text-end">
<a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan</button> <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
Kembali
</a>
<button type="submit" class="btn btn-success data-submit wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@include('produktivitas.survey-kelompok-tani.pengurus-anggota')
</div> </div>
@endsection @endsection
@@ -171,6 +178,7 @@
function toggleJenisKelompokFields() { function toggleJenisKelompokFields() {
var jenisKelompok = $('#edit-kt-jenis-kelompok').val(); var jenisKelompok = $('#edit-kt-jenis-kelompok').val();
$('#edit-kt-jenis-kelompok-lainnya').prop('required', jenisKelompok === 'Lainnya').prop('disabled', jenisKelompok !== 'Lainnya');
if (jenisKelompok === 'Lainnya') { if (jenisKelompok === 'Lainnya') {
$('#edit-kt-jenis-kelompok-lainnya-wrapper').show(); $('#edit-kt-jenis-kelompok-lainnya-wrapper').show();
@@ -247,52 +255,6 @@
}); });
$('#edit-kt-desa').select2(); $('#edit-kt-desa').select2();
$('#editKelompokTaniForm').validate({
rules: {
nama_responden: { required: true },
nama_kelompok: { required: true },
provinsi_id: { required: true },
kabupaten_id: { required: true },
kecamatan_id: { required: true },
desa_kelurahan_id: { required: true },
jenis_kelompok: { required: true },
jenis_kelompok_lainnya: {
required: function () {
return $('#edit-kt-jenis-kelompok').val() === 'Lainnya';
}
},
tahun_berdiri: { min: 1900, max: new Date().getFullYear() }
},
messages: {
nama_responden: { required: 'Nama responden wajib diisi' },
nama_kelompok: { required: 'Nama kelompok wajib diisi' },
provinsi_id: { required: 'Provinsi wajib diisi' },
kabupaten_id: { required: 'Kabupaten/Kota wajib diisi' },
kecamatan_id: { required: 'Kecamatan wajib diisi' },
desa_kelurahan_id: { required: 'Desa/Kelurahan wajib diisi' },
jenis_kelompok: { required: 'Jenis kelompok wajib dipilih' },
jenis_kelompok_lainnya: { required: 'Sebutkan jenis kelompoknya' }
},
errorElement: 'div',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
if (element.hasClass('select2-hidden-accessible')) {
element.closest('.form-control-validation').append(error);
} else {
element.closest('.mb-4, .form-control-validation').append(error);
}
},
highlight: function (element) {
$(element).addClass('is-invalid');
},
unhighlight: function (element) {
$(element).removeClass('is-invalid');
},
submitHandler: function (form) {
form.submit();
}
});
}); });
</script> </script>
@endpush @endpush
@@ -1,3 +1,5 @@
@include('produktivitas.survey-kelompok-tani._alert')
<style> <style>
@media (min-width: 768px) { @media (min-width: 768px) {
.nav-pills-custom-spacing { .nav-pills-custom-spacing {
@@ -25,7 +27,7 @@
padding-right: 0.5rem !important; padding-right: 0.5rem !important;
width: 100%; width: 100%;
white-space: nowrap; white-space: nowrap;
font-size: 0.9375rem !important; font-size: var(--bs-body-font-size) !important;
} }
.nav-pills-custom-spacing .nav-link i { .nav-pills-custom-spacing .nav-link i {
@@ -45,33 +47,53 @@
: null; : null;
@endphp @endphp
<div class="card mb-6"> <div class="card mb-6" id="kelompok-tani-summary">
<div class="card-body p-3"> <div class="card-body p-3">
<div class="d-flex justify-content-start align-items-center flex-wrap row-gap-2"> <div class="row align-items-center gy-3">
<div class="avatar avatar-lg me-3"> <div class="{{ isset($kelompokTani) ? 'col-lg-8' : 'col-12' }} d-flex align-items-center">
<span class="avatar-initial rounded bg-label-primary"> <div class="avatar avatar-lg me-3">
<i class="icon-base ti tabler-users-group icon-28px"></i> <span class="avatar-initial rounded bg-label-primary">
</span> <i class="icon-base ti tabler-users-group icon-28px"></i>
</span>
</div>
<div class="d-flex flex-column text-break" style="min-width: 0;">
<h6 class="mb-0 kelompok-tani-summary-name">{{ isset($kelompokTani) ? $kelompokTani->nama_kelompok : 'Tambah Kelompok Tani Baru' }}</h6>
<span class="text-muted kelompok-tani-summary-note">
{{ $lokasiKelompok ?: 'Lengkapi profil untuk memulai' }}
</span>
@if(isset($kelompokTani) && ($kelompokTani->jenis_kelompok || $namaKomoditas->isNotEmpty() || $kelompokTani->nama_gapoktan))
<div class="d-flex flex-wrap gap-1 mt-2">
@if($kelompokTani->jenis_kelompok)
<span class="badge bg-label-secondary">{{ $kelompokTani->jenis_kelompok }}</span>
@endif
@if($kelompokTani->nama_gapoktan)
<span class="badge bg-label-info">Gapoktan: {{ $kelompokTani->nama_gapoktan }}</span>
@endif
@foreach($namaKomoditas as $nama)
<span class="badge bg-label-success">{{ $nama }}</span>
@endforeach
</div>
@endif
</div>
</div> </div>
<div class="d-flex flex-column"> @if(isset($kelompokTani))
<h5 class="mb-0">{{ isset($kelompokTani) ? $kelompokTani->nama_kelompok : 'Tambah Kelompok Tani Baru' }}</h5> <div class="col-lg-4">
<span class="text-muted" style="font-size: 0.8rem;"> <p class="mb-2 fw-medium">Jumlah Pengurus & Anggota</p>
{{ $lokasiKelompok ?: 'Lengkapi identitas & data responden untuk memulai' }} <div class="row g-2">
</span> @foreach([
@if(isset($kelompokTani) && ($kelompokTani->jenis_kelompok || $namaKomoditas->isNotEmpty() || $kelompokTani->nama_gapoktan)) 'jumlah_pengurus' => 'Total Pengurus',
<div class="d-flex flex-wrap gap-1 mt-2"> 'jumlah_anggota_terdaftar' => 'Total Anggota',
@if($kelompokTani->jenis_kelompok) ] as $field => $label)
<span class="badge bg-label-secondary">{{ $kelompokTani->jenis_kelompok }}</span> <div class="col-6">
@endif <div class="border rounded p-2 h-100">
@if($kelompokTani->nama_gapoktan) <p class="mb-1 text-muted kelompok-tani-summary-note">{{ $label }}</p>
<span class="badge bg-label-info">Gapoktan: {{ $kelompokTani->nama_gapoktan }}</span> <h5 class="mb-0">{{ $kelompokTani->{$field} !== null ? number_format($kelompokTani->{$field}, 0, ',', '.') : '-' }}</h5>
@endif </div>
@foreach($namaKomoditas as $nama) </div>
<span class="badge bg-label-success">{{ $nama }}</span>
@endforeach @endforeach
</div> </div>
@endif </div>
</div> @endif
</div> </div>
@if(isset($kelompokTani)) @if(isset($kelompokTani))
@@ -84,8 +106,8 @@
</span> </span>
</span> </span>
<div> <div>
<p class="mb-0 text-muted" style="font-size: 0.75rem;">Nama Responden</p> <p class="mb-0 text-muted kelompok-tani-summary-note">Nama Responden</p>
<span class="fw-medium" style="font-size: 0.85rem;">{{ $kelompokTani->nama_responden ?: '-' }}</span> <span class="fw-medium small">{{ $kelompokTani->nama_responden ?: '-' }}</span>
</div> </div>
</div> </div>
<div class="col-sm-6 col-lg-3 d-flex align-items-center"> <div class="col-sm-6 col-lg-3 d-flex align-items-center">
@@ -95,8 +117,8 @@
</span> </span>
</span> </span>
<div> <div>
<p class="mb-0 text-muted" style="font-size: 0.75rem;">Jabatan Responden</p> <p class="mb-0 text-muted kelompok-tani-summary-note">Jabatan Responden</p>
<span class="fw-medium" style="font-size: 0.85rem;">{{ $kelompokTani->jabatan_responden ?: '-' }}</span> <span class="fw-medium small">{{ $kelompokTani->jabatan_responden ?: '-' }}</span>
</div> </div>
</div> </div>
<div class="col-sm-6 col-lg-3 d-flex align-items-center"> <div class="col-sm-6 col-lg-3 d-flex align-items-center">
@@ -106,8 +128,8 @@
</span> </span>
</span> </span>
<div> <div>
<p class="mb-0 text-muted" style="font-size: 0.75rem;">Kontak Responden</p> <p class="mb-0 text-muted kelompok-tani-summary-note">Kontak Responden</p>
<span class="fw-medium" style="font-size: 0.85rem;">{{ $kelompokTani->kontak_responden ?: '-' }}</span> <span class="fw-medium small">{{ $kelompokTani->kontak_responden ?: '-' }}</span>
</div> </div>
</div> </div>
<div class="col-sm-6 col-lg-3 d-flex align-items-center"> <div class="col-sm-6 col-lg-3 d-flex align-items-center">
@@ -117,8 +139,8 @@
</span> </span>
</span> </span>
<div> <div>
<p class="mb-0 text-muted" style="font-size: 0.75rem;">Tahun Berdiri</p> <p class="mb-0 text-muted kelompok-tani-summary-note">Tahun Berdiri</p>
<span class="fw-medium" style="font-size: 0.85rem;">{{ $kelompokTani->tahun_berdiri ?: '-' }}</span> <span class="fw-medium small">{{ $kelompokTani->tahun_berdiri ?: '-' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -131,22 +153,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ request()->routeIs('kelompok-tani.create') || (request()->routeIs('kelompok-tani.edit') && !request()->query('section')) ? 'active' : '' }}" <a class="nav-link {{ request()->routeIs('kelompok-tani.create') || (request()->routeIs('kelompok-tani.edit') && !request()->query('section')) ? 'active' : '' }}"
href="{{ isset($kelompokTani) ? route('kelompok-tani.edit', $kelompokTani->id) : route('kelompok-tani.create') }}"> href="{{ isset($kelompokTani) ? route('kelompok-tani.edit', $kelompokTani->id) : route('kelompok-tani.create') }}">
<i class="icon-base ti tabler-id-badge-2 me-1_5 icon-sm"></i>Identitas & Responden <i class="icon-base ti tabler-user-check me-1_5 icon-sm"></i>Profil
</a> </a>
</li> </li>
<li class="nav-item">
@if(isset($kelompokTani))
<a class="nav-link {{ request()->routeIs('kelompok-tani.pengurus-anggota.*') ? 'active' : '' }}"
href="{{ route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id) }}">
<i class="icon-base ti tabler-users-group me-1_5 icon-sm"></i>Pengurus & Anggota
</a>
@else
<a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Simpan Identitas & Responden terlebih dahulu">
<i class="icon-base ti tabler-users-group me-1_5 icon-sm"></i>Pengurus & Anggota
</a>
@endif
</li>
<li class="nav-item"> <li class="nav-item">
@if(isset($kelompokTani)) @if(isset($kelompokTani))
<a class="nav-link {{ request()->routeIs('kelompok-tani.edit') && request()->query('section') === 'kelembagaan' ? 'active' : '' }}" <a class="nav-link {{ request()->routeIs('kelompok-tani.edit') && request()->query('section') === 'kelembagaan' ? 'active' : '' }}"
@@ -155,7 +164,7 @@
</a> </a>
@else @else
<a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top" <a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Simpan Identitas & Responden terlebih dahulu"> data-bs-original-title="Simpan Profil terlebih dahulu">
<i class="icon-base ti tabler-building-community me-1_5 icon-sm"></i>Kelembagaan <i class="icon-base ti tabler-building-community me-1_5 icon-sm"></i>Kelembagaan
</a> </a>
@endif @endif
@@ -164,25 +173,12 @@
@if(isset($kelompokTani)) @if(isset($kelompokTani))
<a class="nav-link {{ request()->routeIs('kelompok-tani.edit') && request()->query('section') === 'aktivitas-fungsi' ? 'active' : '' }}" <a class="nav-link {{ request()->routeIs('kelompok-tani.edit') && request()->query('section') === 'aktivitas-fungsi' ? 'active' : '' }}"
href="{{ route('kelompok-tani.edit', $kelompokTani->id) }}?section=aktivitas-fungsi"> href="{{ route('kelompok-tani.edit', $kelompokTani->id) }}?section=aktivitas-fungsi">
<i class="icon-base ti tabler-clipboard-list me-1_5 icon-sm"></i>Aktivitas & Fungsi <i class="icon-base ti tabler-clipboard-list me-1_5 icon-sm"></i>Operasional
</a> </a>
@else @else
<a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top" <a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Simpan Identitas & Responden terlebih dahulu"> data-bs-original-title="Simpan Profil terlebih dahulu">
<i class="icon-base ti tabler-clipboard-list me-1_5 icon-sm"></i>Aktivitas & Fungsi <i class="icon-base ti tabler-clipboard-list me-1_5 icon-sm"></i>Operasional
</a>
@endif
</li>
<li class="nav-item">
@if(isset($kelompokTani))
<a class="nav-link {{ request()->routeIs('kelompok-tani.aset-keuangan.*') ? 'active' : '' }}"
href="{{ route('kelompok-tani.aset-keuangan.index', $kelompokTani->id) }}">
<i class="icon-base ti tabler-wallet me-1_5 icon-sm"></i>Aset & Keuangan
</a>
@else
<a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Simpan Identitas & Responden terlebih dahulu">
<i class="icon-base ti tabler-wallet me-1_5 icon-sm"></i>Aset & Keuangan
</a> </a>
@endif @endif
</li> </li>
@@ -190,25 +186,12 @@
@if(isset($kelompokTani)) @if(isset($kelompokTani))
<a class="nav-link {{ request()->routeIs('kelompok-tani.edit') && request()->query('section') === 'sosial-konflik' ? 'active' : '' }}" <a class="nav-link {{ request()->routeIs('kelompok-tani.edit') && request()->query('section') === 'sosial-konflik' ? 'active' : '' }}"
href="{{ route('kelompok-tani.edit', $kelompokTani->id) }}?section=sosial-konflik"> href="{{ route('kelompok-tani.edit', $kelompokTani->id) }}?section=sosial-konflik">
<i class="icon-base ti tabler-heart-handshake me-1_5 icon-sm"></i>Sosial & Konflik <i class="icon-base ti tabler-heart-handshake me-1_5 icon-sm"></i>Sosial
</a> </a>
@else @else
<a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top" <a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Simpan Identitas & Responden terlebih dahulu"> data-bs-original-title="Simpan Profil terlebih dahulu">
<i class="icon-base ti tabler-heart-handshake me-1_5 icon-sm"></i>Sosial & Konflik <i class="icon-base ti tabler-heart-handshake me-1_5 icon-sm"></i>Sosial
</a>
@endif
</li>
<li class="nav-item">
@if(isset($kelompokTani))
<a class="nav-link {{ request()->routeIs('kelompok-tani.bantuan-penilaian.*') ? 'active' : '' }}"
href="{{ route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id) }}">
<i class="icon-base ti tabler-report me-1_5 icon-sm"></i>Bantuan & Penilaian
</a>
@else
<a class="nav-link" href="#" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Simpan Identitas & Responden terlebih dahulu">
<i class="icon-base ti tabler-report me-1_5 icon-sm"></i>Bantuan & Penilaian
</a> </a>
@endif @endif
</li> </li>
@@ -1,29 +1,179 @@
@extends('layout.main') @extends('layout.main')
@push('style')
<style>
.kelompok-tani-row { cursor: pointer; }
.kelompok-filter-row { padding-block: 1.5rem; }
.kelompok-filter-line { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; }
.kelompok-filter-label { display: flex; align-items: center; font-weight: 500; margin-right: .5rem; white-space: nowrap; }
.kelompok-filter-line .form-select,
.kelompok-filter-line .btn { min-height: 31px; }
.kelompok-filter-line .form-select {
background-color: var(--bs-paper-bg);
border-color: var(--bs-border-color);
border-radius: var(--bs-border-radius);
color: var(--bs-body-color);
}
.kelompok-filter-line .form-select:focus {
border-color: rgba(40, 167, 69, .55);
box-shadow: 0 0 0 .2rem rgba(40, 167, 69, .12);
}
.kelompok-filter-line .btn { white-space: nowrap; }
.kelompok-filter-static {
display: flex; align-items: center; min-height: 31px;
padding: .25rem .75rem; font-size: .8125rem;
background-color: var(--bs-paper-bg);
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
color: var(--bs-body-color);
}
.kelompok-filter-line .input-group-sm { width: 240px; }
@media (max-width: 767.98px) {
.kelompok-filter-line { display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; }
.kelompok-filter-label { margin-right: 0; }
.kelompok-filter-line .btn { justify-content: center; width: 100%; }
.kelompok-filter-line .input-group-sm { width: 100%; }
.kelompok-filter-line .input-group .btn { width: auto; }
}
</style>
@endpush
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-fluid flex-grow-1 container-p-y kelompok-tani-page">
@include('produktivitas.survey-kelompok-tani._alert')
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<x-summary-card
title="Kelompok Tani"
count="{{ number_format($summary->total_kelompok, 0, ',', '.') }}"
subtitle="{{ number_format($summary->total_jenis, 0, ',', '.') }} jenis kelompok"
icon="users-group"
color="primary"
/>
</div>
<div class="col-12 col-md-4">
<x-summary-card
title="Total Pengurus"
count="{{ number_format($summary->total_pengurus, 0, ',', '.') }}"
subtitle="{{ number_format($summary->pengurus_aktif, 0, ',', '.') }} pengurus aktif"
icon="user-star"
color="success"
/>
</div>
<div class="col-12 col-md-4">
<x-summary-card
title="Total Anggota"
count="{{ number_format($summary->total_anggota, 0, ',', '.') }}"
subtitle="{{ number_format($summary->anggota_aktif, 0, ',', '.') }} anggota aktif"
icon="users"
color="info"
/>
</div>
</div>
<div class="row g-6 kelompok-filter-row">
<div class="col-12">
<div class="card card-border-shadow-primary">
<div class="card-body py-3">
<form method="GET" action="{{ route('kelompok-tani.index') }}" id="kelompok-filter-form" aria-label="Filter data kelompok tani">
<input type="hidden" name="per_page" value="{{ $kelompokTani->perPage() }}">
<div class="kelompok-filter-line">
<div class="kelompok-filter-label">
<i class="icon-base ti tabler-map-2 me-2"></i><span>Wilayah</span>
</div>
<div>
<input type="hidden" name="kabupaten_kota_id" value="{{ $filters['kabupaten_kota_id'] }}">
<div class="kelompok-filter-static" aria-label="Kabupaten/Kota" role="text">
{{ preg_replace('/^(kabupaten|kota)\s+/i', '', $selectedKabupaten?->nama ?? 'Semua Kabupaten/Kota') }}
</div>
</div>
<div>
<select name="kecamatan_id" id="filter-kecamatan" class="form-select form-select-sm" aria-label="Filter Kecamatan" @disabled(empty($filters['kabupaten_kota_id']))>
<option value="">Semua Kecamatan</option>
@foreach($kecamatanOptions as $option)
<option value="{{ $option->id }}" @selected((string) $filters['kecamatan_id'] === (string) $option->id)>{{ $option->nama }}</option>
@endforeach
</select>
</div>
<div>
<select name="desa_kelurahan_id" id="filter-desa" class="form-select form-select-sm" aria-label="Filter Desa" @disabled(empty($filters['kecamatan_id']))>
<option value="">Semua Desa</option>
@foreach($desaOptions as $option)
<option value="{{ $option->id }}" @selected((string) $filters['desa_kelurahan_id'] === (string) $option->id)>{{ $option->nama }}</option>
@endforeach
</select>
</div>
</div>
<hr class="my-3">
<div class="kelompok-filter-line">
<div class="kelompok-filter-label">
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i><span>Data</span>
</div>
<div>
<select name="komoditas_id" class="form-select form-select-sm" aria-label="Filter Komoditas">
<option value="">Semua Komoditas</option>
@foreach($komoditasOptions as $option)
<option value="{{ $option->id }}" @selected((string) $filters['komoditas_id'] === (string) $option->id)>{{ $option->nama }}</option>
@endforeach
</select>
</div>
<div>
<select name="jenis_kelompok" class="form-select form-select-sm" aria-label="Filter Jenis Kelompok">
<option value="">Semua Jenis Kelompok</option>
@foreach($jenisKelompokOptions as $jenis)
<option value="{{ $jenis }}" @selected($filters['jenis_kelompok'] === $jenis)>{{ $jenis }}</option>
@endforeach
</select>
</div>
<div>
<select name="sort_by" class="form-select form-select-sm" aria-label="Urutkan data kelompok tani">
<option value="">Urut Terbaru</option>
<option value="nama_asc" @selected($filters['sort_by'] === 'nama_asc')>Nama A-Z</option>
<option value="nama_desc" @selected($filters['sort_by'] === 'nama_desc')>Nama Z-A</option>
<option value="anggota_desc" @selected($filters['sort_by'] === 'anggota_desc')>Anggota Terbanyak</option>
<option value="anggota_asc" @selected($filters['sort_by'] === 'anggota_asc')>Anggota Tersedikit</option>
</select>
</div>
<div>
<div class="input-group input-group-sm">
<input type="text" name="search" class="form-control" placeholder="Cari kelompok / responden..." value="{{ $filters['search'] }}" aria-label="Cari nama kelompok atau responden">
<button type="submit" class="btn btn-outline-secondary" aria-label="Cari data kelompok tani"><i class="icon-base ti tabler-search"></i></button>
</div>
</div>
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="icon-base ti tabler-filter me-1"></i>Filter</button>
<a href="{{ route('kelompok-tani.index') }}" class="btn btn-outline-secondary btn-sm"><i class="icon-base ti tabler-refresh me-1"></i>Reset</a>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header border-bottom"> <div class="card-header border-bottom">
<h5 class="card-title mb-0">Data Kelompok Tani</h5> <h5 class="card-title mb-0">Data Kelompok Tani</h5>
</div> </div>
<div class="card-body border-bottom pt-4"> <div class="card-body border-bottom pt-4">
<form method="GET" action="{{ route('kelompok-tani.index') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0"> <form method="GET" action="{{ route('kelompok-tani.index') }}" class="row g-3 align-items-end">
@foreach($filters as $field => $value)
<input type="hidden" name="{{ $field }}" value="{{ $value }}">
@endforeach
<div class="col-md-auto"> <div class="col-md-auto">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<label class="me-2 text-nowrap">Show</label> <label class="me-2 text-nowrap">Show</label>
<select name="per_page" class="form-select form-select-sm w-auto" onchange="this.form.submit()"> <select name="per_page" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
<option value="10" {{ request('per_page') == 10 ? 'selected' : '' }}>10</option> <option value="10" @selected($kelompokTani->perPage() === 10)>10</option>
<option value="25" {{ request('per_page') == 25 ? 'selected' : '' }}>25</option> <option value="25" @selected($kelompokTani->perPage() === 25)>25</option>
</select> </select>
<label class="ms-2 text-nowrap">entries</label> <label class="ms-2 text-nowrap">entries</label>
</div> </div>
</div> </div>
<div class="col-md-auto d-flex gap-2"> <div class="col-md-auto ms-md-auto">
<div class="input-group input-group-sm w-auto">
<input type="text" name="search" class="form-control" placeholder="Cari Nama..." value="{{ request('search') }}">
<button type="submit" class="btn btn-outline-primary"><i class="ti tabler-search"></i></button>
</div>
<a href="{{ route('kelompok-tani.create') }}" class="btn btn-sm btn-primary"> <a href="{{ route('kelompok-tani.create') }}" class="btn btn-sm btn-primary">
<i class="ti tabler-plus me-1"></i> Tambah <i class="ti tabler-plus me-1"></i> Tambah
</a> </a>
@@ -46,9 +196,9 @@
</thead> </thead>
<tbody> <tbody>
@forelse($kelompokTani as $index => $item) @forelse($kelompokTani as $index => $item)
<tr> <tr class="kelompok-tani-row">
<td>{{ $loop->iteration }}</td> <td>{{ $kelompokTani->firstItem() + $loop->index }}</td>
<td><span class="text-heading">{{ $item->nama_kelompok }}</span></td> <td><a href="{{ route('kelompok-tani.edit', $item->id) }}" class="text-heading d-block">{{ $item->nama_kelompok }}</a></td>
<td><span class="text-muted">{{ $item->desaKelurahan?->nama }}</span></td> <td><span class="text-muted">{{ $item->desaKelurahan?->nama }}</span></td>
<td><span class="text-muted">{{ $item->jenis_kelompok }}</span></td> <td><span class="text-muted">{{ $item->jenis_kelompok }}</span></td>
<td><span class="text-muted">{{ $item->tahun_berdiri }}</span></td> <td><span class="text-muted">{{ $item->tahun_berdiri }}</span></td>
@@ -67,7 +217,7 @@
</table> </table>
</div> </div>
<div class="card-footer d-flex justify-content-center justify-content-md-end border-top"> <div class="card-footer d-flex justify-content-center justify-content-md-end border-top">
{{ $kelompokTani->appends(request()->query())->links('pagination::bootstrap-5') }} {{ $kelompokTani->links('pagination::bootstrap-5') }}
</div> </div>
</div> </div>
</div> </div>
@@ -79,6 +229,37 @@
const csrfToken = "{{ csrf_token() }}"; const csrfToken = "{{ csrf_token() }}";
$(document).ready(function () { $(document).ready(function () {
const $kecamatanFilter = $('#filter-kecamatan');
const $desaFilter = $('#filter-desa');
const desaUrlTemplate = '{{ route("desakelurahan.bykecamatan", ":id") }}';
let desaRequest;
$kecamatanFilter.on('change', function () {
if (desaRequest) desaRequest.abort();
$desaFilter.empty().append(new Option('Semua Desa', '')).prop('disabled', true);
const kecamatanId = $(this).val();
if (!kecamatanId) return;
desaRequest = $.get(desaUrlTemplate.replace(':id', kecamatanId), function (data) {
data.forEach(function (desa) {
$desaFilter.append(new Option(desa.nama, desa.id));
});
$desaFilter.prop('disabled', false);
}).fail(function (xhr, status) {
if (status !== 'abort') {
$desaFilter.empty().append(new Option('Gagal memuat desa', ''));
}
});
});
$(document).on('click', '.kelompok-tani-row', function (event) {
if ($(event.target).closest('a, button, input, select, textarea, label').length) {
return;
}
window.location.href = this.querySelector('.edit-record').href;
});
$(document).on('click', '.delete-record', function () { $(document).on('click', '.delete-record', function () {
const id = $(this).data('id'); const id = $(this).data('id');
const name = $(this).data('nama'); const name = $(this).data('nama');
@@ -1,17 +1,12 @@
@extends('layout.main') @extends('layout.main')
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y kelompok-tani-page">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani]) @include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani])
<div class="card mb-6" id="section-kelembagaan"> <div class="card mb-6" id="section-kelembagaan">
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [ <form novalidate method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
'section' => 'kelembagaan',
'errorFields' => ['cakupan_wilayah', 'nama_wilayah_cakupan', 'status_legalitas', 'nomor_sk', 'tanggal_tahun_penerbitan_sk', 'tingkat_keaktifan_ketua', 'status_pembagian_peran', 'status_dokumentasi_rapat', 'frekuensi_pertemuan', 'tanggal_pertemuan_terakhir'],
])
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
@csrf @csrf
@method('PUT') @method('PUT')
<input type="hidden" name="section" value="kelembagaan"> <input type="hidden" name="section" value="kelembagaan">
@@ -20,7 +15,7 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 form-control-validation"> <div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="klb-cakupan-wilayah">Cakupan Wilayah Keanggotaan</label> <label class="form-label" for="klb-cakupan-wilayah">Cakupan Wilayah Keanggotaan</label>
<select class="form-control @error('cakupan_wilayah') is-invalid @enderror" id="klb-cakupan-wilayah" name="cakupan_wilayah"> <select class="form-select @error('cakupan_wilayah') is-invalid @enderror" id="klb-cakupan-wilayah" name="cakupan_wilayah">
<option value="">Pilih Cakupan Wilayah</option> <option value="">Pilih Cakupan Wilayah</option>
@foreach(['RT', 'RW', 'Desa', 'Kecamatan', 'Kabupaten'] as $option) @foreach(['RT', 'RW', 'Desa', 'Kecamatan', 'Kabupaten'] as $option)
<option value="{{ $option }}" {{ old('cakupan_wilayah', $kelompokTani->cakupan_wilayah) == $option ? 'selected' : '' }}>{{ $option }}</option> <option value="{{ $option }}" {{ old('cakupan_wilayah', $kelompokTani->cakupan_wilayah) == $option ? 'selected' : '' }}>{{ $option }}</option>
@@ -37,11 +32,15 @@
</div> </div>
</div> </div>
@php
$statusDenganSk = \App\Models\Pertanian\KelompokTani::STATUS_LEGALITAS_DENGAN_SK;
$skAktif = in_array(old('status_legalitas', $kelompokTani->status_legalitas), $statusDenganSk, true);
@endphp
<h5 class="pb-4 border-bottom mb-4 mt-4">Status Legalitas Kelompok</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Status Legalitas Kelompok</h5>
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="klb-status-legalitas">Status SK/Dokumen Pendaftaran</label> <label class="form-label" for="klb-status-legalitas">Status SK/Dokumen Pendaftaran</label>
<select class="form-control @error('status_legalitas') is-invalid @enderror" id="klb-status-legalitas" name="status_legalitas"> <select class="form-select @error('status_legalitas') is-invalid @enderror" id="klb-status-legalitas" name="status_legalitas">
<option value="">Pilih Status Legalitas</option> <option value="">Pilih Status Legalitas</option>
@foreach([ @foreach([
'Belum ada SK/dokumen apa pun', 'Belum ada SK/dokumen apa pun',
@@ -56,95 +55,28 @@
@error('status_legalitas')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('status_legalitas')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="klb-nomor-sk">Nomor SK (jika ada)</label> <label class="form-label" for="klb-nomor-sk">Nomor SK</label>
<input type="text" class="form-control @error('nomor_sk') is-invalid @enderror" id="klb-nomor-sk" name="nomor_sk" <input type="text" class="form-control @error('nomor_sk') is-invalid @enderror" id="klb-nomor-sk" name="nomor_sk"
value="{{ old('nomor_sk', $kelompokTani->nomor_sk) }}" /> value="{{ old('nomor_sk', $kelompokTani->nomor_sk) }}" @disabled(!$skAktif) />
@error('nomor_sk')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('nomor_sk')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-3 mb-4 form-control-validation"> <div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="klb-tanggal-penerbitan-sk">Tanggal/Tahun Penerbitan</label> <label class="form-label" for="klb-tanggal-penerbitan-sk">Tanggal/Tahun Penerbitan</label>
<input type="text" class="form-control @error('tanggal_tahun_penerbitan_sk') is-invalid @enderror" id="klb-tanggal-penerbitan-sk" name="tanggal_tahun_penerbitan_sk" <input type="text" class="form-control @error('tanggal_tahun_penerbitan_sk') is-invalid @enderror" id="klb-tanggal-penerbitan-sk" name="tanggal_tahun_penerbitan_sk"
placeholder="Cth: 12-01-2020 atau 2020" value="{{ old('tanggal_tahun_penerbitan_sk', $kelompokTani->tanggal_tahun_penerbitan_sk) }}" /> placeholder="Cth: 12-01-2020 atau 2020" value="{{ old('tanggal_tahun_penerbitan_sk', $kelompokTani->tanggal_tahun_penerbitan_sk) }}" @disabled(!$skAktif) />
@error('tanggal_tahun_penerbitan_sk')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('tanggal_tahun_penerbitan_sk')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
</div> </div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Kualitas Kepemimpinan</h5>
<div class="row">
<div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="klb-keaktifan-ketua">Keaktifan Ketua Kelompok</label>
<select class="form-control @error('tingkat_keaktifan_ketua') is-invalid @enderror" id="klb-keaktifan-ketua" name="tingkat_keaktifan_ketua">
<option value="">Pilih Tingkat Keaktifan</option>
@foreach([
'Tidak aktif/jarang hadir',
'Cukup aktif',
'Sangat aktif dan menjalankan tanggung jawab dengan baik',
] as $option)
<option value="{{ $option }}" {{ old('tingkat_keaktifan_ketua', $kelompokTani->tingkat_keaktifan_ketua) == $option ? 'selected' : '' }}>{{ $option }}</option>
@endforeach
</select>
@error('tingkat_keaktifan_ketua')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="klb-pembagian-peran">Pembagian Peran Pengurus</label>
<select class="form-control @error('status_pembagian_peran') is-invalid @enderror" id="klb-pembagian-peran" name="status_pembagian_peran">
<option value="">Pilih Status Pembagian Peran</option>
@foreach([
'Tidak ada pembagian peran',
'Ada tapi tidak berjalan',
'Ada dan berjalan dengan baik',
] as $option)
<option value="{{ $option }}" {{ old('status_pembagian_peran', $kelompokTani->status_pembagian_peran) == $option ? 'selected' : '' }}>{{ $option }}</option>
@endforeach
</select>
@error('status_pembagian_peran')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="klb-dokumentasi-rapat">Dokumentasi Rapat (Notulen/Daftar Hadir)</label>
<select class="form-control @error('status_dokumentasi_rapat') is-invalid @enderror" id="klb-dokumentasi-rapat" name="status_dokumentasi_rapat">
<option value="">Pilih Status Dokumentasi</option>
@foreach([
'Tidak ada',
'Ada, tidak rutin',
'Ada, rutin setiap pertemuan',
] as $option)
<option value="{{ $option }}" {{ old('status_dokumentasi_rapat', $kelompokTani->status_dokumentasi_rapat) == $option ? 'selected' : '' }}>{{ $option }}</option>
@endforeach
</select>
@error('status_dokumentasi_rapat')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Frekuensi Pertemuan Rutin</h5>
<div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="klb-frekuensi-pertemuan">Frekuensi Pertemuan</label>
<select class="form-control @error('frekuensi_pertemuan') is-invalid @enderror" id="klb-frekuensi-pertemuan" name="frekuensi_pertemuan">
<option value="">Pilih Frekuensi Pertemuan</option>
@foreach([
'Tidak pernah/sudah tidak aktif',
'Tidak rutin/tidak terjadwal',
'1x per 2-3 bulan',
'1x per bulan',
'Lebih dari 1x per bulan',
] as $option)
<option value="{{ $option }}" {{ old('frekuensi_pertemuan', $kelompokTani->frekuensi_pertemuan) == $option ? 'selected' : '' }}>{{ $option }}</option>
@endforeach
</select>
@error('frekuensi_pertemuan')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="klb-tanggal-pertemuan-terakhir">Tanggal Pertemuan Terakhir</label>
<input type="text" class="form-control @error('tanggal_pertemuan_terakhir') is-invalid @enderror" id="klb-tanggal-pertemuan-terakhir" name="tanggal_pertemuan_terakhir"
value="{{ old('tanggal_pertemuan_terakhir', $kelompokTani->tanggal_pertemuan_terakhir?->format('Y-m-d')) }}" />
@error('tanggal_pertemuan_terakhir')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<div class="mt-4 text-end"> <div class="mt-4 text-end">
<button type="submit" class="btn btn-success wave-effect wave-light"> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<i class="ti tabler-device-floppy me-2"></i> Simpan Kelembagaan <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
</button> Kembali
</a>
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
@@ -156,12 +88,15 @@
<script> <script>
'use strict'; 'use strict';
$(document).ready(function () { $(document).ready(function () {
$('#klb-cakupan-wilayah, #klb-status-legalitas, #klb-keaktifan-ketua, #klb-pembagian-peran, #klb-dokumentasi-rapat, #klb-frekuensi-pertemuan').select2(); $('#klb-cakupan-wilayah, #klb-status-legalitas').select2();
$('#klb-tanggal-pertemuan-terakhir').flatpickr({ const statusDenganSk = @json($statusDenganSk);
dateFormat: 'Y-m-d', function toggleSkFields() {
altInput: true, const skAktif = statusDenganSk.includes($('#klb-status-legalitas').val());
altFormat: 'd-m-Y' $('#klb-nomor-sk, #klb-tanggal-penerbitan-sk').prop('disabled', !skAktif);
}); }
$('#klb-status-legalitas').on('change', toggleSkFields);
toggleSkFields();
}); });
</script> </script>
@endpush @endpush
@@ -1,5 +1,65 @@
@extends('layout.main') @if(($bagianKeanggotaan ?? 'pengurus') === 'ringkasan')
<div id="section-ringkasan" class="mt-4">
<h5 class="pb-4 border-bottom mb-4">Jumlah Pengurus & Anggota</h5>
<div class="row">
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-pengurus">Jumlah Pengurus (total)</label>
<input type="number" min="0" class="form-control @error('jumlah_pengurus') is-invalid @enderror" id="pa-jumlah-pengurus" name="jumlah_pengurus"
value="{{ old('jumlah_pengurus', $kelompokTani?->jumlah_pengurus) }}" />
@error('jumlah_pengurus')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-pengurus-aktif">Jumlah Pengurus Aktif</label>
<input type="number" min="0" class="form-control @error('jumlah_pengurus_aktif') is-invalid @enderror" id="pa-jumlah-pengurus-aktif" name="jumlah_pengurus_aktif"
value="{{ old('jumlah_pengurus_aktif', $kelompokTani?->jumlah_pengurus_aktif) }}" />
@error('jumlah_pengurus_aktif')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-anggota-terdaftar">Jumlah Anggota Terdaftar</label>
<input type="number" min="0" class="form-control @error('jumlah_anggota_terdaftar') is-invalid @enderror" id="pa-jumlah-anggota-terdaftar" name="jumlah_anggota_terdaftar"
value="{{ old('jumlah_anggota_terdaftar', $kelompokTani?->jumlah_anggota_terdaftar) }}" />
@error('jumlah_anggota_terdaftar')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-anggota-aktif">Jumlah Anggota Aktif <small class="text-muted">(hadir pertemuan rutin)</small></label>
<input type="number" min="0" class="form-control @error('jumlah_anggota_aktif') is-invalid @enderror" id="pa-jumlah-anggota-aktif" name="jumlah_anggota_aktif"
value="{{ old('jumlah_anggota_aktif', $kelompokTani?->jumlah_anggota_aktif) }}" />
@error('jumlah_anggota_aktif')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Komposisi Gender</h5>
<div class="row">
<div class="col-md-4 mb-4">
<label class="form-label d-block">Jumlah Pengurus (Laki-laki / Perempuan)</label>
<div class="d-flex gap-2">
<input type="number" min="0" class="form-control @error('jumlah_pengurus_laki_laki') is-invalid @enderror" name="jumlah_pengurus_laki_laki"
placeholder="Laki-laki" value="{{ old('jumlah_pengurus_laki_laki', $kelompokTani?->jumlah_pengurus_laki_laki) }}" />
<input type="number" min="0" class="form-control @error('jumlah_pengurus_perempuan') is-invalid @enderror" name="jumlah_pengurus_perempuan"
placeholder="Perempuan" value="{{ old('jumlah_pengurus_perempuan', $kelompokTani?->jumlah_pengurus_perempuan) }}" />
</div>
</div>
<div class="col-md-4 mb-4">
<label class="form-label d-block">Jumlah Anggota (Laki-laki / Perempuan)</label>
<div class="d-flex gap-2">
<input type="number" min="0" class="form-control @error('jumlah_anggota_laki_laki') is-invalid @enderror" name="jumlah_anggota_laki_laki"
placeholder="Laki-laki" value="{{ old('jumlah_anggota_laki_laki', $kelompokTani?->jumlah_anggota_laki_laki) }}" />
<input type="number" min="0" class="form-control @error('jumlah_anggota_perempuan') is-invalid @enderror" name="jumlah_anggota_perempuan"
placeholder="Perempuan" value="{{ old('jumlah_anggota_perempuan', $kelompokTani?->jumlah_anggota_perempuan) }}" />
</div>
</div>
<div class="col-md-4 mb-4">
<label class="form-label d-block">Jumlah Anggota Pemuda (16-30 th) (Laki-laki / Perempuan)</label>
<div class="d-flex gap-2">
<input type="number" min="0" class="form-control @error('jumlah_anggota_pemuda_laki_laki') is-invalid @enderror" name="jumlah_anggota_pemuda_laki_laki"
placeholder="Laki-laki" value="{{ old('jumlah_anggota_pemuda_laki_laki', $kelompokTani?->jumlah_anggota_pemuda_laki_laki) }}" />
<input type="number" min="0" class="form-control @error('jumlah_anggota_pemuda_perempuan') is-invalid @enderror" name="jumlah_anggota_pemuda_perempuan"
placeholder="Perempuan" value="{{ old('jumlah_anggota_pemuda_perempuan', $kelompokTani?->jumlah_anggota_pemuda_perempuan) }}" />
</div>
</div>
</div>
</div>
@else
@push('style') @push('style')
<style> <style>
.status-switch-row { .status-switch-row {
@@ -26,99 +86,9 @@
</style> </style>
@endpush @endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani])
{{-- RINGKASAN JUMLAH & KOMPOSISI GENDER --}}
<div class="card mb-6" id="section-ringkasan">
<div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [
'section' => 'ringkasan',
'errorFields' => ['jumlah_pengurus', 'jumlah_pengurus_aktif', 'jumlah_anggota_terdaftar', 'jumlah_anggota_aktif', 'jumlah_pengurus_laki_laki', 'jumlah_pengurus_perempuan', 'jumlah_anggota_laki_laki', 'jumlah_anggota_perempuan', 'jumlah_anggota_pemuda_laki_laki', 'jumlah_anggota_pemuda_perempuan'],
])
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
@csrf
@method('PUT')
<input type="hidden" name="section" value="ringkasan-pengurus">
<h5 class="pb-4 border-bottom mb-4">Jumlah Pengurus & Anggota</h5>
<div class="row">
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-pengurus">Jumlah Pengurus (total)</label>
<input type="number" min="0" class="form-control @error('jumlah_pengurus') is-invalid @enderror" id="pa-jumlah-pengurus" name="jumlah_pengurus"
value="{{ old('jumlah_pengurus', $kelompokTani->jumlah_pengurus) }}" />
@error('jumlah_pengurus')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-pengurus-aktif">Jumlah Pengurus Aktif</label>
<input type="number" min="0" class="form-control @error('jumlah_pengurus_aktif') is-invalid @enderror" id="pa-jumlah-pengurus-aktif" name="jumlah_pengurus_aktif"
value="{{ old('jumlah_pengurus_aktif', $kelompokTani->jumlah_pengurus_aktif) }}" />
@error('jumlah_pengurus_aktif')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-anggota-terdaftar">Jumlah Anggota Terdaftar</label>
<input type="number" min="0" class="form-control @error('jumlah_anggota_terdaftar') is-invalid @enderror" id="pa-jumlah-anggota-terdaftar" name="jumlah_anggota_terdaftar"
value="{{ old('jumlah_anggota_terdaftar', $kelompokTani->jumlah_anggota_terdaftar) }}" />
@error('jumlah_anggota_terdaftar')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="pa-jumlah-anggota-aktif">Jumlah Anggota Aktif <small class="text-muted">(hadir pertemuan rutin)</small></label>
<input type="number" min="0" class="form-control @error('jumlah_anggota_aktif') is-invalid @enderror" id="pa-jumlah-anggota-aktif" name="jumlah_anggota_aktif"
value="{{ old('jumlah_anggota_aktif', $kelompokTani->jumlah_anggota_aktif) }}" />
@error('jumlah_anggota_aktif')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Komposisi Gender</h5>
<div class="row">
<div class="col-md-4 mb-4">
<label class="form-label d-block">Jumlah Pengurus (Laki-laki / Perempuan)</label>
<div class="d-flex gap-2">
<input type="number" min="0" class="form-control @error('jumlah_pengurus_laki_laki') is-invalid @enderror" name="jumlah_pengurus_laki_laki"
placeholder="Laki-laki" value="{{ old('jumlah_pengurus_laki_laki', $kelompokTani->jumlah_pengurus_laki_laki) }}" />
<input type="number" min="0" class="form-control @error('jumlah_pengurus_perempuan') is-invalid @enderror" name="jumlah_pengurus_perempuan"
placeholder="Perempuan" value="{{ old('jumlah_pengurus_perempuan', $kelompokTani->jumlah_pengurus_perempuan) }}" />
</div>
</div>
<div class="col-md-4 mb-4">
<label class="form-label d-block">Jumlah Anggota (Laki-laki / Perempuan)</label>
<div class="d-flex gap-2">
<input type="number" min="0" class="form-control @error('jumlah_anggota_laki_laki') is-invalid @enderror" name="jumlah_anggota_laki_laki"
placeholder="Laki-laki" value="{{ old('jumlah_anggota_laki_laki', $kelompokTani->jumlah_anggota_laki_laki) }}" />
<input type="number" min="0" class="form-control @error('jumlah_anggota_perempuan') is-invalid @enderror" name="jumlah_anggota_perempuan"
placeholder="Perempuan" value="{{ old('jumlah_anggota_perempuan', $kelompokTani->jumlah_anggota_perempuan) }}" />
</div>
</div>
<div class="col-md-4 mb-4">
<label class="form-label d-block">Jumlah Anggota Pemuda (16-30 th) <small class="text-muted">L / P</small></label>
<div class="d-flex gap-2">
<input type="number" min="0" class="form-control @error('jumlah_anggota_pemuda_laki_laki') is-invalid @enderror" name="jumlah_anggota_pemuda_laki_laki"
placeholder="Laki-laki" value="{{ old('jumlah_anggota_pemuda_laki_laki', $kelompokTani->jumlah_anggota_pemuda_laki_laki) }}" />
<input type="number" min="0" class="form-control @error('jumlah_anggota_pemuda_perempuan') is-invalid @enderror" name="jumlah_anggota_pemuda_perempuan"
placeholder="Perempuan" value="{{ old('jumlah_anggota_pemuda_perempuan', $kelompokTani->jumlah_anggota_pemuda_perempuan) }}" />
</div>
</div>
</div>
<div class="mt-4 text-end">
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="ti tabler-device-floppy me-2"></i> Simpan Ringkasan
</button>
</div>
</form>
</div>
</div>
{{-- DAFTAR PENGURUS --}} {{-- DAFTAR PENGURUS --}}
<div class="card mb-6" id="section-pengurus"> <div class="card mb-6" id="section-pengurus">
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [
'section' => 'pengurus',
'errorFields' => ['pengurus.*'],
])
<h5 class="pb-4 border-bottom mb-4">Daftar Pengurus</h5> <h5 class="pb-4 border-bottom mb-4">Daftar Pengurus</h5>
{{-- DATA LAMA --}} {{-- DATA LAMA --}}
@@ -159,7 +129,7 @@
@endif @endif
{{-- FORM TAMBAH DATA BARU --}} {{-- FORM TAMBAH DATA BARU --}}
<form method="POST" action="{{ route('kelompok-tani.pengurus-anggota.store', $kelompokTani->id) }}" class="form-repeater-pengurus"> <form novalidate method="POST" action="{{ route('kelompok-tani.pengurus-anggota.store', $kelompokTani->id) }}" class="form-repeater-pengurus">
@csrf @csrf
<div data-repeater-list="pengurus"> <div data-repeater-list="pengurus">
@@ -200,19 +170,22 @@
</div> </div>
</div> </div>
<div class="mt-4 d-flex justify-content-between"> <div class="mt-4 d-flex flex-wrap justify-content-between gap-2">
<button type="button" class="btn btn-outline-success" id="btn-tambah-pengurus" data-repeater-create disabled> <button type="button" class="btn btn-outline-success" id="btn-tambah-pengurus" data-repeater-create disabled>
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambah Pengurus <i class="icon-base ti tabler-plus me-1_5 icon-sm"></i> Tambah Pengurus
</button> </button>
<button type="submit" class="btn btn-success"> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i> Simpan Daftar Pengurus <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
</button> Kembali
</a>
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div>
@endsection
@push('scripts') @push('scripts')
<script> <script>
@@ -316,3 +289,4 @@ $(function () {
}); });
</script> </script>
@endpush @endpush
@endif
@@ -1,17 +1,18 @@
@extends('layout.main') @extends('layout.main')
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y kelompok-tani-page">
@include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani]) @include('produktivitas.survey-kelompok-tani.header', ['kelompokTani' => $kelompokTani])
@php
$pernahKonflik = in_array(old('konflik_kelompok', $kelompokTani->konflik_kelompok), [
'Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai',
], true);
@endphp
<div class="card mb-6" id="section-sosial-konflik"> <div class="card mb-6" id="section-sosial-konflik">
<div class="card-body pt-6"> <div class="card-body pt-6">
@include('produktivitas.survey-kelompok-tani._alert', [ <form novalidate method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
'section' => 'sosial-konflik',
'errorFields' => ['partisipasi_perempuan', 'konflik_kelompok', 'topik_konflik', 'penyelesaian_konflik'],
])
<form method="POST" action="{{ route('kelompok-tani.update', $kelompokTani->id) }}">
@csrf @csrf
@method('PUT') @method('PUT')
<input type="hidden" name="section" value="sosial-konflik"> <input type="hidden" name="section" value="sosial-konflik">
@@ -20,7 +21,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="sk-partisipasi-perempuan">Apakah Perempuan Terlibat Aktif dalam Kegiatan & Pengambilan Keputusan?</label> <label class="form-label" for="sk-partisipasi-perempuan">Apakah Perempuan Terlibat Aktif dalam Kegiatan & Pengambilan Keputusan?</label>
<select class="form-control @error('partisipasi_perempuan') is-invalid @enderror" id="sk-partisipasi-perempuan" name="partisipasi_perempuan"> <select class="form-select @error('partisipasi_perempuan') is-invalid @enderror" id="sk-partisipasi-perempuan" name="partisipasi_perempuan">
<option value="">Pilih Status</option> <option value="">Pilih Status</option>
@foreach([ @foreach([
'Tidak terlibat', 'Tidak terlibat',
@@ -35,13 +36,10 @@
</div> </div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Riwayat Konflik Internal Kelompok</h5> <h5 class="pb-4 border-bottom mb-4 mt-4">Riwayat Konflik Internal Kelompok</h5>
<p class="text-muted" style="font-size: 0.8rem; margin-top: -0.75rem;">
Topik ini cukup sensitif. Kalau responden tidak nyaman menjawab, pilih "Tidak bersedia menjawab".
</p>
<div class="row"> <div class="row">
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="sk-konflik-kelompok">Pernah Terjadi Konflik Internal?</label> <label class="form-label" for="sk-konflik-kelompok">Pernah Terjadi Konflik Internal?</label>
<select class="form-control @error('konflik_kelompok') is-invalid @enderror" id="sk-konflik-kelompok" name="konflik_kelompok"> <select class="form-select @error('konflik_kelompok') is-invalid @enderror" id="sk-konflik-kelompok" name="konflik_kelompok">
<option value="">Pilih Status</option> <option value="">Pilih Status</option>
@foreach([ @foreach([
'Tidak pernah', 'Tidak pernah',
@@ -56,11 +54,11 @@
</div> </div>
</div> </div>
<div class="row" id="sk-detail-konflik-wrapper" style="display: none;"> <div class="row" id="sk-detail-konflik-wrapper" @if(!$pernahKonflik) style="display: none;" @endif>
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="sk-topik-konflik">Jika Pernah, Soal Apa?</label> <label class="form-label" for="sk-topik-konflik">Jika Pernah, Soal Apa? <span class="text-danger" id="sk-topik-konflik-required" @if(!$pernahKonflik) hidden @endif>*</span></label>
<input type="text" class="form-control @error('topik_konflik') is-invalid @enderror" id="sk-topik-konflik" name="topik_konflik" <input type="text" class="form-control @error('topik_konflik') is-invalid @enderror" id="sk-topik-konflik" name="topik_konflik"
placeholder="Cth: Kepemimpinan, pembagian hasil" value="{{ old('topik_konflik', $kelompokTani->topik_konflik) }}" /> placeholder="Contoh: Kepemimpinan, pembagian hasil" value="{{ old('topik_konflik', $kelompokTani->topik_konflik) }}" @required($pernahKonflik) @disabled(!$pernahKonflik) />
@error('topik_konflik')<div class="error invalid-feedback">{{ $message }}</div>@enderror @error('topik_konflik')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="col-md-6 mb-4 form-control-validation"> <div class="col-md-6 mb-4 form-control-validation">
@@ -71,9 +69,14 @@
</div> </div>
<div class="mt-4 text-end"> <div class="mt-4 text-end">
<button type="submit" class="btn btn-success wave-effect wave-light"> <div class="d-inline-flex flex-wrap justify-content-end gap-2">
<i class="ti tabler-device-floppy me-2"></i> Simpan Sosial & Konflik <a href="{{ route('kelompok-tani.index') }}" class="btn btn-secondary wave-effect wave-light">
</button> Kembali
</a>
<button type="submit" class="btn btn-success wave-effect wave-light">
<i class="icon-base ti tabler-check me-1_5 icon-sm" aria-hidden="true"></i> Simpan
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
@@ -89,7 +92,10 @@
function toggleDetailKonflik() { function toggleDetailKonflik() {
var val = $('#sk-konflik-kelompok').val(); var val = $('#sk-konflik-kelompok').val();
$('#sk-detail-konflik-wrapper').toggle(val === 'Pernah, sudah selesai' || val === 'Pernah, masih berlanjut/belum selesai'); var pernah = val === 'Pernah, sudah selesai' || val === 'Pernah, masih berlanjut/belum selesai';
$('#sk-detail-konflik-wrapper').toggle(pernah);
$('#sk-topik-konflik').prop('required', pernah).prop('disabled', !pernah);
$('#sk-topik-konflik-required').prop('hidden', !pernah);
} }
toggleDetailKonflik(); toggleDetailKonflik();
$('#sk-konflik-kelompok').on('change', toggleDetailKonflik); $('#sk-konflik-kelompok').on('change', toggleDetailKonflik);
@@ -1,103 +0,0 @@
@extends('layout.main')
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card">
<div class="card-header border-bottom mb-4">
<h5 class="card-title mb-0">Edit Petani</h5>
</div>
<div class="card-body">
<form id="viewPetaniForm">
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<h6 class="mb-3">Informasi Petani</h6>
<div class="row">
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-nama">Nama Petani</label>
<input type="text" class="form-control" id="edit-petani-nama" name="nama"
placeholder="Cth: John Doe" value="{{ old('nama', $petani->nama) }}" readonly/>
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-jenis-kelamin">Jenis Kelamin</label>
<input type="text" class="form-control" id="edit-petani-jenis-kelamin" name="jenis_kelamin"
value="{{ old('jenis_kelamin', $petani->jenis_kelamin) == '1' ? 'Laki-laki' : 'Perempuan' }}" readonly/>
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-tempat-lahir">Tempat Lahir</label>
<input type="text" class="form-control" id="edit-petani-tempat-lahir" name="tempat_lahir" placeholder="Cth: Ngada"
value="{{ old('tempat_lahir', $petani->tempat_lahir) }}" />
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-tanggal-lahir">Tanggal Lahir</label>
<input type="date" class="form-control" id="edit-petani-tanggal-lahir" name="tanggal_lahir"
value="{{ old('tanggal_lahir', $petani->tanggal_lahir?->format('d-m-Y')) }}" />
</div>
</div>
<div class="row">
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-pendidikan-terakhir">Pendidikan Terakhir</label>
<input type="text" class="form-control" id="edit-petani-pendidikan-terakhir" name="pendidikan_terakhir"
value="{{ old('pendidikan_terakhir', $petani->pendidikan_terakhir?->label()) }}" readonly/>
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-status-pernikahan">Status Pernikahan<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="edit-petani-status-pernikahan" name="status_perkawinan"
value="{{ old('status_perkawinan', $petani->status_perkawinan?->label()) }}" readonly/>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-alamat">Alamat</label>
<input type="text" class="form-control" id="edit-petani-alamat" name="alamat"
placeholder="Cth: Alamat lengkap petani" value="{{ old('alamat', $petani->alamat) }}" />
</div>
</div>
<div class="row">
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-provinsi">Provinsi</label>
<input type="text" class="form-control" id="edit-petani-provinsi" name="provinsi"
value="{{ old('provinsi', $petani->desaKelurahan?->kecamatan->kabupatenKota->provinsi->nama) }}" readonly/>
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-kabupaten">Kabupaten/Kota</label>
<input type="text" class="form-control" id="edit-petani-kabupaten" name="kabupaten"
value="{{ old('kabupaten', $petani->desaKelurahan?->kecamatan->kabupatenKota->nama) }}" readonly/>
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-kecamatan">Kecamatan</label>
<input type="text" class="form-control" id="edit-petani-kecamatan" name="kecamatan"
value="{{ old('kecamatan', $petani->desaKelurahan?->kecamatan->nama) }}" readonly/>
</div>
<div class="col-md-3 mb-4 form-control-validation">
<label class="form-label" for="edit-petani-desa">Desa/Kelurahan</label>
<input type="text" class="form-control" id="edit-petani-desa" name="desa"
value="{{ old('desa', $petani->desaKelurahan->nama) }}" readonly/>
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}" class="btn btn-label-secondary me-2">Kembali</a>
</div>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
'use strict';
$(document).ready(function () {
});
</script>
@endpush
+1
View File
@@ -43,6 +43,7 @@ Route::middleware('auth')
Route::get('/dashboard/produktivitas/map-information', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'mapInformation'])->name('dashboard.produktivitas.map-information'); Route::get('/dashboard/produktivitas/map-information', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'mapInformation'])->name('dashboard.produktivitas.map-information');
Route::get('/dashboard/produktivitas', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'produktivitas'])->name('dashboard.produktivitas'); Route::get('/dashboard/produktivitas', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'produktivitas'])->name('dashboard.produktivitas');
Route::get('/dashboard/pasar/harga-internasional', App\Http\Controllers\Web\Dashboard\InternationalMarketController::class)->name('dashboard.pasar.international');
Route::get('/dashboard/pasar', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'pasar'])->name('dashboard.pasar'); Route::get('/dashboard/pasar', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'pasar'])->name('dashboard.pasar');
// Dashboard // Dashboard
+147
View File
@@ -0,0 +1,147 @@
<?php
use App\Http\Controllers\Web\Produktivitas\SurveyKelompokTaniController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
beforeEach(function () {
foreach ([
'master_kabupaten_kota' => null,
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
'master_komoditas' => null,
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->string('nama');
if ($parent) $table->integer($parent);
$table->softDeletes();
});
}
Schema::create('survey_kelompok_tani', function (Blueprint $table) {
$table->id();
$table->string('nama_kelompok');
$table->string('nama_responden')->nullable();
$table->integer('desa_kelurahan_id');
$table->string('jenis_kelompok')->nullable();
$table->json('komoditas_id')->nullable();
foreach (['jumlah_pengurus', 'jumlah_pengurus_aktif', 'jumlah_anggota_terdaftar', 'jumlah_anggota_aktif'] as $field) {
$table->integer($field)->nullable();
}
$table->timestamps();
$table->softDeletes();
});
DB::table('master_kabupaten_kota')->insert([
['id' => 1, 'nama' => 'Kabupaten Ngada'], ['id' => 2, 'nama' => 'Kabupaten Lain'],
]);
DB::table('master_kecamatan')->insert([
['id' => 1, 'nama' => 'Kecamatan A', 'kabupaten_kota_id' => 1],
['id' => 2, 'nama' => 'Kecamatan B', 'kabupaten_kota_id' => 1],
['id' => 3, 'nama' => 'Kecamatan C', 'kabupaten_kota_id' => 2],
]);
DB::table('master_desa_kelurahan')->insert([
['id' => 1, 'nama' => 'Desa A', 'kecamatan_id' => 1],
['id' => 2, 'nama' => 'Desa B', 'kecamatan_id' => 1],
['id' => 3, 'nama' => 'Desa C', 'kecamatan_id' => 2],
['id' => 4, 'nama' => 'Desa D', 'kecamatan_id' => 3],
]);
DB::table('master_komoditas')->insert([
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Kopi'], ['id' => 11, 'nama' => 'Vanili'],
]);
foreach ([
[1, 'Alpha', 'Budi', 1, 'Poktan', ['1', '2'], 3, 2, 30, 25, null],
[2, 'Beta', 'Siti', 2, 'KWT', [1], 5, 4, 40, 35, null],
[3, 'Citra', 'Ani', 3, 'Poktan', [2, 11], null, null, 0, 0, null],
[4, 'Diluar', 'Toni', 4, 'Poktan', [1], 99, 99, 900, 900, null],
[5, 'Dihapus', 'Dewi', 1, 'Poktan', [1], 99, 99, 900, 900, '2026-01-01'],
] as [$id, $nama, $responden, $desa, $jenis, $komoditas, $pengurus, $pengurusAktif, $anggota, $anggotaAktif, $deletedAt]) {
DB::table('survey_kelompok_tani')->insert([
'id' => $id, 'nama_kelompok' => $nama, 'nama_responden' => $responden,
'desa_kelurahan_id' => $desa, 'jenis_kelompok' => $jenis,
'komoditas_id' => json_encode($komoditas), 'jumlah_pengurus' => $pengurus,
'jumlah_pengurus_aktif' => $pengurusAktif, 'jumlah_anggota_terdaftar' => $anggota,
'jumlah_anggota_aktif' => $anggotaAktif, 'created_at' => "2026-01-0{$id}", 'deleted_at' => $deletedAt,
]);
}
$this->listKelompok = function (array $filters = []) {
$request = Request::create('/survey/kelompok-tani', 'GET', $filters);
$this->app->instance('request', $request);
return app(SurveyKelompokTaniController::class)->index($request)->getData();
};
});
test('kelompok cards summarize the default region and exclude deleted groups', function () {
$data = ($this->listKelompok)();
expect($data['kelompokTani']->pluck('id')->all())->toBe([3, 2, 1]);
expect((int) $data['summary']->total_kelompok)->toBe(3)
->and((int) $data['summary']->total_jenis)->toBe(2)
->and((int) $data['summary']->total_pengurus)->toBe(8)
->and((int) $data['summary']->pengurus_aktif)->toBe(6)
->and((int) $data['summary']->total_anggota)->toBe(70)
->and((int) $data['summary']->anggota_aktif)->toBe(60);
expect($data['kecamatanOptions']->pluck('id')->all())->toBe([1, 2]);
});
test('kelompok region filters constrain rows cards and dependent options', function () {
$data = ($this->listKelompok)(['kecamatan_id' => 1, 'desa_kelurahan_id' => 2]);
expect($data['kelompokTani']->pluck('id')->all())->toBe([2]);
expect((int) $data['summary']->total_anggota)->toBe(40);
expect($data['desaOptions']->pluck('id')->all())->toBe([1, 2]);
expect(($this->listKelompok)(['kecamatan_id' => 2])['kelompokTani']->pluck('id')->all())->toBe([3]);
expect(($this->listKelompok)(['kabupaten_kota_id' => 2])['kelompokTani']->pluck('id')->all())->toBe([4]);
});
test('kelompok commodity filter matches string and numeric JSON ids exactly', function () {
$data = ($this->listKelompok)(['komoditas_id' => '1']);
expect($data['kelompokTani']->pluck('id')->all())->toBe([2, 1]);
expect((int) $data['summary']->total_kelompok)->toBe(2);
expect(($this->listKelompok)(['komoditas_id' => 2, 'jenis_kelompok' => 'Poktan'])['kelompokTani']->pluck('id')->all())->toBe([3, 1]);
expect(($this->listKelompok)(['jenis_kelompok' => 'KWT'])['kelompokTani']->pluck('id')->all())->toBe([2]);
});
test('kelompok search matches group and respondent names without escaping region filters', function () {
expect(($this->listKelompok)(['search' => 'aLpHa'])['kelompokTani']->pluck('id')->all())->toBe([1]);
expect(($this->listKelompok)(['search' => 'sITI'])['kelompokTani']->pluck('id')->all())->toBe([2]);
$data = ($this->listKelompok)(['search' => 'Toni']);
expect($data['kelompokTani']->total())->toBe(0);
expect((int) $data['summary']->total_pengurus)->toBe(0)
->and((int) $data['summary']->total_anggota)->toBe(0);
});
test('kelompok sorting supports names membership totals and safe fallback', function () {
foreach ([
'nama_asc' => [1, 2, 3], 'nama_desc' => [3, 2, 1],
'anggota_desc' => [2, 1, 3], 'anggota_asc' => [3, 1, 2], 'unknown' => [3, 2, 1],
] as $sort => $ids) {
expect(($this->listKelompok)(['sort_by' => $sort])['kelompokTani']->pluck('id')->all())->toBe($ids);
}
});
test('kelompok totals cover every result and pagination preserves all filters', function () {
for ($index = 0; $index < 12; $index++) {
DB::table('survey_kelompok_tani')->insert([
'nama_kelompok' => 'Kelompok tambahan ' . $index, 'desa_kelurahan_id' => 1,
'jenis_kelompok' => 'Poktan', 'komoditas_id' => '["1"]', 'jumlah_anggota_terdaftar' => 10,
]);
}
$data = ($this->listKelompok)(['komoditas_id' => 1, 'sort_by' => 'nama_asc', 'page' => 2, 'per_page' => '10']);
expect($data['kelompokTani']->count())->toBe(4);
expect($data['kelompokTani']->perPage())->toBe(10);
expect((int) $data['summary']->total_kelompok)->toBe(14)
->and((int) $data['summary']->total_anggota)->toBe(190);
parse_str(parse_url($data['kelompokTani']->url(1), PHP_URL_QUERY), $params);
expect($params)->toMatchArray(['komoditas_id' => '1', 'sort_by' => 'nama_asc', 'per_page' => '10']);
});
test('kelompok filters reject malformed ids and unsupported page sizes', function (array $filters) {
expect(fn () => ($this->listKelompok)($filters))->toThrow(ValidationException::class);
})->with([
[['kecamatan_id' => 'invalid']], [['komoditas_id' => 999]], [['per_page' => 0]], [['search' => ['invalid']]],
]);
+942
View File
@@ -0,0 +1,942 @@
<?php
use App\Http\Controllers\Web\Produktivitas\PengurusKelompokTaniController;
use App\Http\Controllers\Web\Produktivitas\AsetKelompokTaniController;
use App\Http\Controllers\Web\Produktivitas\BantuanKelompokTaniController;
use App\Http\Controllers\Web\Produktivitas\SurveyKelompokTaniController;
use App\Models\Pertanian\KelompokTani;
use App\Models\Pertanian\AsetKelompokTani;
use App\Models\Pertanian\BantuanKelompokTani;
use App\Models\User;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ViewErrorBag;
use Illuminate\Validation\ValidationException;
beforeEach(function () {
foreach ([
'master_provinsi' => null,
'master_kabupaten_kota' => 'provinsi_id',
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
'master_komoditas' => null,
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->string('nama');
$table->string('kode')->nullable();
$table->boolean('is_utama')->default(true);
if ($parent) $table->integer($parent);
$table->softDeletes();
});
}
Schema::create('survey_kelompok_tani', function (Blueprint $table) {
$table->id();
foreach ([
'uid', 'nama_kelompok', 'nama_responden', 'jabatan_responden', 'kontak_responden',
'dusun', 'jenis_kelompok', 'nama_gapoktan', 'cakupan_wilayah', 'nama_wilayah_cakupan',
'status_legalitas', 'nomor_sk', 'tanggal_tahun_penerbitan_sk', 'frekuensi_pertemuan',
'tanggal_pertemuan_terakhir',
'pembukuan_kas', 'pengelola_kas', 'masalah_pengelolaan_kas_simpan_pinjam', 'deskripsi_masalah_pengelolaan_kas_simpan_pinjam',
'potensi_kelompok', 'alasan_penilaian_potensi', 'konflik_kelompok', 'topik_konflik', 'penyelesaian_konflik', 'partisipasi_perempuan',
] as $field) {
$table->string($field)->nullable();
}
foreach ([
'desa_kelurahan_id', 'tahun_berdiri', 'jumlah_pengurus', 'jumlah_pengurus_aktif',
'jumlah_anggota_terdaftar', 'jumlah_anggota_aktif', 'jumlah_pengurus_laki_laki',
'jumlah_pengurus_perempuan', 'jumlah_anggota_laki_laki', 'jumlah_anggota_perempuan',
'jumlah_anggota_pemuda_laki_laki', 'jumlah_anggota_pemuda_perempuan',
'created_by', 'updated_by', 'deleted_by',
] as $field) {
$table->integer($field)->nullable();
}
$table->json('komoditas_id')->nullable();
$table->json('kegiatan_terlaksana')->nullable();
$table->json('fungsi_kelompok_bagi_anggota')->nullable();
$table->boolean('pernah_mendapatkan_bantuan')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('survey_pengurus_kelompok_tani', function (Blueprint $table) {
$table->id();
$table->string('uid');
$table->integer('survey_kelompok_tani_id');
$table->string('nama_pengurus');
$table->string('jabatan');
$table->string('kontak');
$table->boolean('status');
$table->integer('created_by')->nullable();
$table->integer('updated_by')->nullable();
$table->integer('deleted_by')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('survey_aset_kelompok_tani', function (Blueprint $table) {
$table->id();
$table->string('uid');
$table->integer('survey_kelompok_tani_id');
$table->string('jenis_aset');
$table->boolean('status_aset');
$table->string('status_pengelolaan_aset');
$table->string('kondisi_aset');
$table->integer('created_by')->nullable();
$table->integer('updated_by')->nullable();
$table->integer('deleted_by')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('survey_bantuan_kelompok_tani', function (Blueprint $table) {
$table->id();
$table->string('uid');
$table->integer('survey_kelompok_tani_id');
$table->string('nama_program');
$table->string('penyelenggara');
$table->string('bentuk_bantuan');
$table->integer('tahun')->nullable();
$table->integer('created_by')->nullable();
$table->integer('updated_by')->nullable();
$table->integer('deleted_by')->nullable();
$table->timestamps();
$table->softDeletes();
});
DB::table('master_provinsi')->insert(['id' => 1, 'nama' => 'NTT']);
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'nama' => 'Ngada', 'provinsi_id' => 1]);
DB::table('master_kecamatan')->insert(['id' => 1, 'nama' => 'Kecamatan A', 'kabupaten_kota_id' => 1]);
DB::table('master_desa_kelurahan')->insert(['id' => 1, 'nama' => 'Desa A', 'kecamatan_id' => 1]);
$user = new User(['name' => 'Admin Test', 'email' => 'admin@example.test', 'role' => 'admin']);
$user->id = 7;
$this->be($user);
Event::forget('composing: *');
view()->share('errors', new ViewErrorBag());
$this->kelompok = fn (array $attributes = []) => KelompokTani::create([
'nama_kelompok' => 'Kelompok A', 'nama_responden' => 'Responden A',
'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)', ...$attributes,
]);
$this->profileRequest = function (array $input = [], string $method = 'PUT') {
$request = Request::create('/survey/kelompok-tani/1/edit', $method, $input);
$request->setLaravelSession(app('session')->driver());
$this->app->instance('request', $request);
return $request;
};
$this->profileDom = function ($view) {
$dom = new DOMDocument();
$previous = libxml_use_internal_errors(true);
$dom->loadHTML($view->render());
libxml_clear_errors();
libxml_use_internal_errors($previous);
return new DOMXPath($dom);
};
});
test('profile saves identity and membership together on creation and update', function () {
$controller = app(SurveyKelompokTaniController::class);
$input = [
'nama_kelompok' => 'Kelompok Baru', 'nama_responden' => 'Budi',
'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)',
'jumlah_pengurus' => 4, 'jumlah_pengurus_aktif' => 3, 'jumlah_anggota_terdaftar' => 20,
'jumlah_anggota_aktif' => 18, 'jumlah_pengurus_laki_laki' => 2, 'jumlah_pengurus_perempuan' => 2,
'jumlah_anggota_laki_laki' => 12, 'jumlah_anggota_perempuan' => 8,
'jumlah_anggota_pemuda_laki_laki' => 5, 'jumlah_anggota_pemuda_perempuan' => 3,
];
$locations = ['provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1];
$controller->store(($this->profileRequest)([...$input, ...$locations], 'POST'));
$group = KelompokTani::firstOrFail();
foreach ($input as $field => $value) expect($group->{$field})->toBe($value);
$response = $controller->update(($this->profileRequest)([
...$input, ...$locations, 'nama_kelompok' => 'Kelompok Diubah', 'jumlah_anggota_terdaftar' => 25,
]), $group);
expect($group->refresh()->nama_kelompok)->toBe('Kelompok Diubah')
->and($group->jumlah_anggota_terdaftar)->toBe(25);
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-pengurus');
});
test('invalid membership prevents partial profile saves', function () {
$group = ($this->kelompok)(['jumlah_anggota_terdaftar' => 20]);
$request = ($this->profileRequest)([
'nama_kelompok' => 'Tidak boleh tersimpan', 'nama_responden' => 'Budi',
'provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1,
'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)',
'jumlah_anggota_terdaftar' => -1,
]);
expect(fn () => app(SurveyKelompokTaniController::class)->update($request, $group))
->toThrow(ValidationException::class);
expect($group->refresh()->nama_kelompok)->toBe('Kelompok A')
->and($group->jumlah_anggota_terdaftar)->toBe(20);
});
test('membership fields belong to the profile form on create and edit', function () {
$controller = app(SurveyKelompokTaniController::class);
($this->profileRequest)([], 'GET');
$create = ($this->profileDom)($controller->create());
expect($create->query('//form[@id="addKelompokTaniForm"]//input[@name="jumlah_anggota_terdaftar"]')->length)->toBe(1);
$group = ($this->kelompok)(['jumlah_anggota_terdaftar' => 20]);
$group->pengurus()->create([
'uid' => 'PKT-TEST', 'nama_pengurus' => 'Ketua A', 'jabatan' => 'Ketua', 'kontak' => '08123456789', 'status' => true,
]);
$edit = ($this->profileDom)($controller->edit(($this->profileRequest)([], 'GET'), $group));
expect($edit->query('//form[@id="editKelompokTaniForm"]//input[@name="jumlah_anggota_terdaftar" and @value="20"]')->length)->toBe(1);
expect($edit->query('//div[@id="section-pengurus"]//input[@value="Ketua A"]')->length)->toBe(1);
expect($edit->query('//form[contains(@class,"form-repeater-pengurus")]')->length)->toBe(1);
expect($edit->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Keanggotaan"]')->length)->toBe(0);
});
test('SK fields render enabled only for statuses with an existing SK', function (?string $status, bool $enabled) {
$group = ($this->kelompok)(['status_legalitas' => $status]);
$view = app(SurveyKelompokTaniController::class)->edit(
($this->profileRequest)(['section' => 'kelembagaan'], 'GET'), $group
);
$dom = ($this->profileDom)($view);
foreach (['klb-nomor-sk', 'klb-tanggal-penerbitan-sk'] as $id) {
expect($dom->query('//input[@id="' . $id . '"]')->item(0)->hasAttribute('disabled'))->toBe(!$enabled);
}
expect($dom->query('//label[@for="klb-nomor-sk"]')->item(0)->textContent)->toBe('Nomor SK');
})->with([
[null, false], ['Belum ada SK/dokumen apa pun', false], ['Sedang proses pengajuan', false],
['Tidak tahu', false], ['Sudah ada SK dari desa/kecamatan', true],
['Sudah ada SK dan terdaftar di Dinas/Kabupaten', true],
]);
test('inactive SK input is ignored and existing values survive status changes', function () {
$group = ($this->kelompok)(['nomor_sk' => 'SK-LAMA', 'tanggal_tahun_penerbitan_sk' => '2020']);
$controller = app(SurveyKelompokTaniController::class);
$controller->update(($this->profileRequest)([
'section' => 'kelembagaan', 'status_legalitas' => 'Tidak tahu',
'nomor_sk' => ['invalid'], 'tanggal_tahun_penerbitan_sk' => str_repeat('x', 100),
]), $group);
expect($group->refresh()->nomor_sk)->toBe('SK-LAMA')
->and($group->tanggal_tahun_penerbitan_sk)->toBe('2020');
$controller->update(($this->profileRequest)([
'section' => 'kelembagaan', 'status_legalitas' => 'Sudah ada SK dari desa/kecamatan',
'nomor_sk' => 'SK-BARU', 'tanggal_tahun_penerbitan_sk' => '2026',
]), $group);
expect($group->refresh()->nomor_sk)->toBe('SK-BARU')
->and($group->tanggal_tahun_penerbitan_sk)->toBe('2026');
});
test('pengurus actions and the former membership URL return to profile', function () {
$group = ($this->kelompok)();
$controller = app(PengurusKelompokTaniController::class);
$response = $controller->index($group);
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-ringkasan');
$response = $controller->store(($this->profileRequest)(['pengurus' => [[
'nama_pengurus' => 'Ketua A', 'jabatan' => 'Ketua', 'kontak' => '08123456789', 'status' => 1,
]]], 'POST'), $group);
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-pengurus');
$pengurus = $group->pengurus()->firstOrFail();
$response = $controller->destroy($group, $pengurus);
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-pengurus');
expect($group->pengurus()->count())->toBe(0);
});
test('meeting fields are shown in Kegiatan with existing values and removed from Kelembagaan', function () {
$group = ($this->kelompok)([
'frekuensi_pertemuan' => '1x per bulan', 'tanggal_pertemuan_terakhir' => '2026-10-20',
]);
$controller = app(SurveyKelompokTaniController::class);
$kelembagaan = ($this->profileDom)($controller->edit(
($this->profileRequest)(['section' => 'kelembagaan'], 'GET'), $group
));
expect($kelembagaan->query('//select[@name="frekuensi_pertemuan"] | //input[@name="tanggal_pertemuan_terakhir"]')->length)->toBe(0);
$aktivitas = ($this->profileDom)($controller->edit(
($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'), $group
));
expect($aktivitas->query('//form//select[@name="frekuensi_pertemuan"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('1x per bulan');
expect($aktivitas->query('//form//input[@name="tanggal_pertemuan_terakhir"]')->item(0)->getAttribute('value'))->toBe('20-10-2026');
expect($aktivitas->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Operasional"]')->length)->toBe(1);
expect($aktivitas->query('//input[@name="tanggal_pertemuan_terakhir"]')->item(0)->hasAttribute('placeholder'))->toBeFalse();
});
test('Kegiatan saves meeting details and rejects invalid dates without partial updates', function () {
$group = ($this->kelompok)();
$controller = app(SurveyKelompokTaniController::class);
$response = $controller->update(($this->profileRequest)([
'section' => 'aktivitas-fungsi', 'frekuensi_pertemuan' => '1x per bulan',
'tanggal_pertemuan_terakhir' => '20-10-2026', 'kegiatan_terlaksana' => ['Pertemuan/diskusi rutin'],
]), $group);
expect($group->refresh()->frekuensi_pertemuan)->toBe('1x per bulan')
->and($group->tanggal_pertemuan_terakhir->format('Y-m-d'))->toBe('2026-10-20')
->and($group->kegiatan_terlaksana)->toBe(['Pertemuan/diskusi rutin']);
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-aset-baku');
$invalid = ($this->profileRequest)([
'section' => 'aktivitas-fungsi', 'frekuensi_pertemuan' => 'Tidak rutin/tidak terjadwal',
'tanggal_pertemuan_terakhir' => 'invalid-date',
]);
expect(fn () => $controller->update($invalid, $group))->toThrow(ValidationException::class);
expect($group->refresh()->frekuensi_pertemuan)->toBe('1x per bulan')
->and($group->tanggal_pertemuan_terakhir->format('Y-m-d'))->toBe('2026-10-20');
$controller->update(($this->profileRequest)([
'section' => 'kelembagaan', 'cakupan_wilayah' => 'Desa', 'frekuensi_pertemuan' => 'Diabaikan',
'tanggal_pertemuan_terakhir' => 'invalid-date',
]), $group);
expect($group->refresh()->cakupan_wilayah)->toBe('Desa')
->and($group->frekuensi_pertemuan)->toBe('1x per bulan');
});
test('all group tabs show Simpan with a check and Kembali to the group list', function () {
($this->profileRequest)([], 'GET');
$group = ($this->kelompok)();
foreach (['pengurus', 'aset', 'bantuan'] as $relation) $group->setRelation($relation, collect());
foreach (['add', 'edit', 'kelembagaan', 'aktivitas-fungsi', 'sosial-konflik'] as $template) {
$dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.' . $template, [
'kelompokTani' => $template === 'add' ? null : $group,
'provinsi' => collect(), 'komoditas' => collect(),
]));
$buttons = $dom->query('//div[contains(@class,"kelompok-tani-page")]//button[@type="submit" and contains(@class,"btn-success")]');
expect($buttons->length)->toBeGreaterThan(0);
foreach ($buttons as $button) {
$form = $dom->query('ancestor::form', $button)->item(0);
expect($form->hasAttribute('novalidate'))->toBeTrue();
expect(trim($button->textContent))->toBe('Simpan');
expect($dom->query('.//i[contains(@class,"tabler-check")]', $button)->length)->toBe(1);
$back = $dom->query('./a[normalize-space()="Kembali"]', $button->parentNode);
expect($back->length)->toBe(1);
expect($back->item(0)->getAttribute('href'))->toBe(route('kelompok-tani.index'));
}
}
});
test('flash notifications appear once above the group summary regardless of section', function (string $key) {
($this->profileRequest)([], 'GET');
session()->flash($key, 'Pesan pemeriksaan');
session()->flash($key . '_section', 'kas');
$group = ($this->kelompok)();
$group->setRelation('pengurus', collect());
$dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.edit', [
'kelompokTani' => $group, 'provinsi' => collect(), 'komoditas' => collect(),
]));
expect($dom->query('//div[@id="kelompok-tani-notifications"]')->length)->toBe(1);
expect($dom->query('//div[@id="kelompok-tani-summary"]/preceding-sibling::div[@id="kelompok-tani-notifications"]')->length)->toBe(1);
expect($dom->query('//div[@id="kelompok-tani-notifications"]//div[@role="alert"]')->length)->toBe(1);
expect($dom->query('//div[@id="kelompok-tani-notifications"]')->item(0)->textContent)->toContain('Pesan pemeriksaan');
})->with(['success', 'error', 'warning', 'info', 'status', 'message']);
test('validation errors are summarized above the group summary', function () {
($this->profileRequest)([], 'GET');
$errors = new ViewErrorBag();
$errors->put('default', new Illuminate\Support\MessageBag(['nomor_sk' => 'Nomor SK tidak valid.']));
view()->share('errors', $errors);
$dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.kelembagaan', [
'kelompokTani' => ($this->kelompok)(),
]));
expect($dom->query('//div[@id="kelompok-tani-summary"]/preceding-sibling::div[@id="kelompok-tani-notifications"]')->length)->toBe(1);
expect($dom->query('//div[@id="kelompok-tani-notifications"]//li')->item(0)->textContent)->toBe('Nomor SK tidak valid.');
});
test('one asset submission saves standard and additional assets and skips empty rows', function () {
$group = ($this->kelompok)();
$controller = app(AsetKelompokTaniController::class);
$response = $controller->update(($this->profileRequest)([
'aset' => [[
'jenis_aset' => 'Timbangan', 'status_aset' => 1,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]],
'aset_lainnya' => [
['jenis_aset' => 'Mesin pengering', 'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi'],
['jenis_aset' => '', 'status_pengelolaan_aset' => '', 'kondisi_aset' => ''],
],
]), $group);
expect($group->aset()->count())->toBe(2);
expect($group->aset()->where('jenis_aset', 'Timbangan')->firstOrFail()->status_pengelolaan_aset)->toBe('Hibah');
expect($group->aset()->where('jenis_aset', 'Mesin pengering')->firstOrFail()->kondisi_aset)->toBe('Berfungsi');
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-status-bantuan');
});
test('invalid additional assets prevent standard assets from being changed', function () {
$group = ($this->kelompok)();
$existing = $group->aset()->create([
'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => true,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]);
$request = ($this->profileRequest)([
'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => 0]],
'aset_lainnya' => [['jenis_aset' => 'Timbangan']],
]);
expect(fn () => app(AsetKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class);
expect($existing->fresh()->trashed())->toBeFalse();
expect($group->aset()->count())->toBe(1);
});
test('combined asset save restores and removes standard assets while keeping additional assets', function () {
$group = ($this->kelompok)();
$controller = app(AsetKelompokTaniController::class);
$existing = $group->aset()->create([
'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => true,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]);
$existing->delete();
$controller->update(($this->profileRequest)([
'aset' => [[
'jenis_aset' => 'Timbangan', 'status_aset' => 1,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]],
'aset_lainnya' => [['jenis_aset' => 'Mesin pengering']],
]), $group);
expect($existing->fresh()->trashed())->toBeFalse();
expect($group->aset()->withTrashed()->where('jenis_aset', 'Timbangan')->count())->toBe(1);
$controller->update(($this->profileRequest)([
'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => 0]],
]), $group);
expect($existing->fresh()->trashed())->toBeTrue();
expect($group->aset()->pluck('jenis_aset')->all())->toBe(['Mesin pengering']);
});
test('asset save rolls back standard changes if additional asset creation fails', function () {
$group = ($this->kelompok)();
$existing = $group->aset()->create([
'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => true,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]);
Event::listen('eloquent.creating: ' . AsetKelompokTani::class, function ($asset) {
if ($asset->jenis_aset === 'Mesin gagal') throw new RuntimeException('Simulated failure');
});
$request = ($this->profileRequest)([
'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => 0]],
'aset_lainnya' => [['jenis_aset' => 'Mesin gagal']],
]);
expect(fn () => app(AsetKelompokTaniController::class)->update($request, $group))->toThrow(RuntimeException::class);
expect($existing->fresh()->trashed())->toBeFalse();
expect($group->aset()->count())->toBe(1);
});
test('asset card has one shared form and preserves inputs after validation errors', function () {
$group = ($this->kelompok)();
$existing = $group->aset()->create([
'uid' => 'AKT-TEST', 'jenis_aset' => 'Mesin lama', 'status_aset' => true,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]);
($this->profileRequest)([], 'GET');
session()->flashInput([
'aset' => [['status_aset' => '1', 'status_pengelolaan_aset' => 'Pinjaman', 'kondisi_aset' => 'Mangkrak']],
'aset_lainnya' => [['jenis_aset' => 'Mesin baru', 'status_pengelolaan_aset' => 'Sewa', 'kondisi_aset' => 'Berfungsi']],
]);
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit(($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'), $group));
expect($dom->query('//div[@id="section-aset-baku"]//form[@id="aset-produktif-form"]')->length)->toBe(1);
expect($dom->query('//form[@id="aset-produktif-form"]//div[@id="section-aset-lainnya"]')->length)->toBe(1);
expect($dom->query('//form[@id="aset-produktif-form"]//button[contains(@class,"btn-success")]')->length)->toBe(1);
expect($dom->query('//form[@id="aset-produktif-form"]//form')->length)->toBe(0);
expect($dom->query('//form[@id="aset-produktif-form"]//input[@value="Mesin baru"]')->length)->toBe(1);
expect($dom->query('//form[@id="aset-produktif-form"]//select[@name="aset[0][status_pengelolaan_aset]"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('Pinjaman');
expect($dom->query('//form[@id="aset-produktif-form"]//select[@name="status_pengelolaan_aset"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('Sewa');
expect($dom->query('//button[@form="delete-aset-' . $existing->id . '"]')->length)->toBe(1);
expect($dom->query('//form[@id="delete-aset-' . $existing->id . '"]')->length)->toBe(1);
});
test('cash management is visible only when the cash asset is present or selected in old input', function (?bool $savedStatus, bool $deleted, ?string $oldStatus, bool $visible) {
$group = ($this->kelompok)();
if ($savedStatus !== null) {
$asset = $group->aset()->create([
'uid' => 'AKT-KAS', 'jenis_aset' => 'Kas/simpanan kelompok', 'status_aset' => $savedStatus,
'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi',
]);
if ($deleted) $asset->delete();
}
($this->profileRequest)([], 'GET');
if ($oldStatus !== null) {
$cashIndex = array_search('Kas/simpanan kelompok', AsetKelompokTaniController::JENIS_ASET_BAKU, true);
session()->flashInput(['aset' => [$cashIndex => ['status_aset' => $oldStatus]]]);
}
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit(($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'), $group));
expect($dom->query('//div[@id="section-kas"]')->item(0)->hasAttribute('hidden'))->toBe(!$visible);
expect($dom->query('//tr[@data-aset-kas]//input[@value="1"]')->item(0)->hasAttribute('checked'))->toBe($visible);
})->with([
[null, false, null, false],
[true, false, null, true],
[false, false, null, false],
[true, true, null, false],
[true, false, '0', false],
[null, false, '1', true],
]);
test('the combined tab contains activities assets and cash management with one header', function () {
$group = ($this->kelompok)();
$group->aset()->create([
'uid' => 'AKT-KAS', 'jenis_aset' => 'Kas/simpanan kelompok', 'status_aset' => true,
'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi',
]);
$request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET');
$route = (new Illuminate\Routing\Route('GET', '/survey/kelompok-tani/{kelompokTani}/edit', fn () => null))->name('kelompok-tani.edit');
$request->setRouteResolver(fn () => $route);
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group));
expect($dom->query('//div[@id="kelompok-tani-summary"]')->length)->toBe(1);
expect($dom->query('//div[@id="section-aktivitas-fungsi"]')->length)->toBe(1);
expect($dom->query('//div[@id="section-aset-baku"]')->length)->toBe(1);
expect($dom->query('//div[@id="section-kas"]')->item(0)->hasAttribute('hidden'))->toBeFalse();
expect($dom->query('//div[@id="section-status-bantuan"]')->length)->toBe(1);
expect($dom->query('//div[@id="section-penilaian"]')->length)->toBe(0);
expect($dom->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Bantuan & Penilaian"]')->length)->toBe(0);
expect($dom->query('//ul[contains(@class,"nav-pills")]//a[contains(@class,"active") and normalize-space()="Operasional"]')->length)->toBe(1);
expect($dom->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Aset & Keuangan"]')->length)->toBe(0);
});
test('asset actions and cash saves return to the combined tab', function () {
$group = ($this->kelompok)();
$controller = app(AsetKelompokTaniController::class);
$url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi';
expect($controller->index($group)->getTargetUrl())->toBe($url . '#section-aset-baku');
$response = $controller->store(($this->profileRequest)([
'aset_lainnya' => [['jenis_aset' => 'Mesin pengering']],
], 'POST'), $group);
expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan');
$response = $controller->destroy($group, $group->aset()->firstOrFail());
expect($response->getTargetUrl())->toBe($url . '#section-aset-lainnya');
$response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([
'section' => 'kas', 'pengelola_kas' => 'Bendahara kelompok',
]), $group);
expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan');
expect($group->refresh()->pengelola_kas)->toBe('Bendahara kelompok');
});
test('present assets require both management status and condition before saving any assets', function ($status) {
$group = ($this->kelompok)();
$request = ($this->profileRequest)([
'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => $status]],
'aset_lainnya' => [['jenis_aset' => 'Mesin pengering']],
]);
try {
app(AsetKelompokTaniController::class)->update($request, $group);
$this->fail('An asset marked Ada must require management status and condition.');
} catch (ValidationException $exception) {
expect(array_keys($exception->errors()))->toBe([
'aset.0.status_pengelolaan_aset', 'aset.0.kondisi_aset',
]);
expect($exception->errors()['aset.0.kondisi_aset'][0])->toBe('Kondisi wajib diisi jika aset dipilih Ada.');
}
expect($group->aset()->count())->toBe(0);
})->with(['1', 1, true]);
test('asset requirements follow each row and reject blank values', function (array $details, string $missingField) {
$group = ($this->kelompok)();
$request = ($this->profileRequest)([
'aset' => [
['jenis_aset' => 'Timbangan', 'status_aset' => '0'],
['jenis_aset' => 'Gudang/tempat simpan', 'status_aset' => '1', ...$details],
],
]);
try {
app(AsetKelompokTaniController::class)->update($request, $group);
$this->fail('The selected asset has a missing detail.');
} catch (ValidationException $exception) {
expect(array_keys($exception->errors()))->toBe(['aset.1.' . $missingField]);
}
expect($group->aset()->count())->toBe(0);
})->with([
[['status_pengelolaan_aset' => '', 'kondisi_aset' => 'Berfungsi'], 'status_pengelolaan_aset'],
[['status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => null], 'kondisi_aset'],
[['status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => ' '], 'kondisi_aset'],
]);
test('asset dropdowns are required only on rows selected Ada', function (bool $present) {
$group = ($this->kelompok)();
$group->aset()->create([
'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => $present,
'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi',
]);
$request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET');
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group));
$index = array_search('Timbangan', AsetKelompokTaniController::JENIS_ASET_BAKU, true);
foreach (['status_pengelolaan_aset', 'kondisi_aset'] as $field) {
$select = $dom->query('//select[@name="aset[' . $index . '][' . $field . ']"]')->item(0);
expect($select->hasAttribute('required'))->toBe($present);
expect($select->hasAttribute('disabled'))->toBe(!$present);
}
})->with([true, false]);
test('asset validation errors appear beside the marked required dropdowns', function () {
$group = ($this->kelompok)();
$request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET');
session()->flashInput(['aset' => [['status_aset' => '1']]]);
$messages = [
'aset.0.status_pengelolaan_aset' => 'Status Pengelolaan wajib diisi jika aset dipilih Ada.',
'aset.0.kondisi_aset' => 'Kondisi wajib diisi jika aset dipilih Ada.',
];
$errors = new ViewErrorBag();
$errors->put('default', new Illuminate\Support\MessageBag($messages));
view()->share('errors', $errors);
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group));
expect($dom->query('//form[@id="aset-produktif-form"]//th/span[contains(@class,"text-danger")]')->length)->toBe(2);
foreach (['status_pengelolaan_aset' => 'status-pengelolaan', 'kondisi_aset' => 'kondisi'] as $field => $idPart) {
$id = 'aset-0-' . $idPart;
$select = $dom->query('//select[@id="' . $id . '"]')->item(0);
expect($select->getAttribute('class'))->toContain('is-invalid');
expect($select->getAttribute('aria-describedby'))->toBe($id . '-error');
expect(trim($dom->query('//div[@id="' . $id . '-error"]')->item(0)->textContent))->toBe($messages['aset.0.' . $field]);
}
});
test('saving assets advances to cash only when the cash section is available', function (bool $hasCash) {
$group = ($this->kelompok)();
$controller = app(AsetKelompokTaniController::class);
$input = ['aset' => [[
'jenis_aset' => 'Kas/simpanan kelompok', 'status_aset' => $hasCash,
'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi',
]]];
$response = $controller->update(($this->profileRequest)($input), $group);
$url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi';
expect($response->getTargetUrl())->toBe($url . ($hasCash ? '#section-kas' : '#section-status-bantuan'));
$input['aset'][0]['status_aset'] = false;
$response = $controller->update(($this->profileRequest)($input), $group);
expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan');
})->with([true, false]);
test('saving aid status stays on the combined aid card in Operasional', function (bool $hasAid) {
$group = ($this->kelompok)();
$response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([
'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => $hasAid,
'bantuan' => $hasAid ? [['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]] : [],
]), $group);
$url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi';
expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan');
})->with([true, false]);
test('legacy aid saves return to Operasional and failed saves retain the notification target', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]);
$controller = app(BantuanKelompokTaniController::class);
$response = $controller->store(($this->profileRequest)(['bantuan' => [[
'nama_program' => 'Bibit Kakao', 'penyelenggara' => 'Dinas Pertanian', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026,
]]], 'POST'), $group);
$url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi';
expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan');
$group->update(['pernah_mendapatkan_bantuan' => false]);
$response = $controller->store(($this->profileRequest)([], 'POST'), $group);
expect($response->getTargetUrl())->toBe($url . '#kelompok-tani-notifications');
expect($group->bantuan()->count())->toBe(1);
});
test('single and final section saves stay on their own box', function () {
$group = ($this->kelompok)();
$controller = app(SurveyKelompokTaniController::class);
foreach ([
'kelembagaan' => ['cakupan_wilayah' => 'Desa'],
'sosial-konflik' => ['konflik_kelompok' => 'Tidak pernah'],
] as $section => $input) {
$response = $controller->update(($this->profileRequest)(['section' => $section, ...$input]), $group);
expect(parse_url($response->getTargetUrl(), PHP_URL_FRAGMENT))->toBe('section-' . $section);
}
});
test('invalid required fields return Indonesian errors above the summary and beside each field', function (string $section, array $payload, array $messages) {
$group = ($this->kelompok)();
$url = route('kelompok-tani.edit', $group->id) . ($section === 'identitas' ? '' : '?section=' . ($section === 'status-bantuan' ? 'aktivitas-fungsi' : $section));
$response = $this->from($url)->put(route('kelompok-tani.update', $group->id), ['section' => $section, ...$payload]);
$response->assertRedirect($url)->assertSessionHasErrors(array_keys($messages));
$page = $this->get($url)->assertOk();
expect(strpos($page->getContent(), 'id="kelompok-tani-notifications"'))->toBeLessThan(strpos($page->getContent(), 'id="kelompok-tani-summary"'));
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($page->getContent());
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$summary = $xpath->query('//div[@id="kelompok-tani-notifications"]')->item(0)->textContent;
$inlineMessages = collect($xpath->query('//div[contains(@class,"invalid-feedback")]'))->map(fn ($node) => trim($node->textContent))->all();
foreach ($messages as $message) {
expect($summary)->toContain($message);
expect($inlineMessages)->toContain($message);
}
expect($xpath->query('//input[contains(@class,"is-invalid")] | //select[contains(@class,"is-invalid")]')->length)->toBeGreaterThan(0);
expect($group->refresh()->nama_kelompok)->toBe('Kelompok A');
})->with([
['identitas', [], [
'nama_responden' => 'Nama responden wajib diisi.', 'nama_kelompok' => 'Nama kelompok wajib diisi.',
'provinsi_id' => 'Provinsi wajib diisi.', 'kabupaten_id' => 'Kabupaten/Kota wajib diisi.',
'kecamatan_id' => 'Kecamatan wajib diisi.', 'desa_kelurahan_id' => 'Desa/Kelurahan wajib dipilih.',
'jenis_kelompok' => 'Terdaftar/Dikenal Sebagai wajib diisi.',
]],
['identitas', ['jenis_kelompok' => 'Lainnya'], ['jenis_kelompok_lainnya' => 'Sebutkan Jenis Kelompok wajib diisi.']],
['status-bantuan', [], ['pernah_mendapatkan_bantuan' => 'Pengalaman Akses Bantuan/Program wajib diisi.']],
['status-bantuan', ['pernah_mendapatkan_bantuan' => '1', 'bantuan' => [[]]], [
'bantuan.0.nama_program' => 'Nama Program/Bantuan pada baris pertama wajib diisi.',
'bantuan.0.penyelenggara' => 'Penyelenggara pada baris pertama wajib diisi.',
'bantuan.0.bentuk_bantuan' => 'Bentuk Bantuan pada baris pertama wajib diisi.',
'bantuan.0.tahun' => 'Tahun pada baris pertama wajib diisi.',
]],
['sosial-konflik', ['konflik_kelompok' => 'Pernah, sudah selesai'], [
'topik_konflik' => 'Jika Pernah, Soal Apa? wajib diisi jika pernah terjadi konflik internal.',
]],
]);
test('asset errors show together above the summary and beside both conditional dropdowns', function () {
$group = ($this->kelompok)();
$url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi';
$this->from($url)->put(route('kelompok-tani.aset-keuangan.update', $group->id), [
'aset' => [['jenis_aset' => AsetKelompokTaniController::JENIS_ASET_BAKU[0], 'status_aset' => '1']],
])->assertRedirect($url)->assertSessionHasErrors(['aset.0.status_pengelolaan_aset', 'aset.0.kondisi_aset']);
$page = $this->get($url)->assertOk();
foreach (['Status Pengelolaan wajib diisi jika aset dipilih Ada.', 'Kondisi wajib diisi jika aset dipilih Ada.'] as $message) {
expect(substr_count($page->getContent(), $message))->toBeGreaterThanOrEqual(2);
}
expect($group->aset()->count())->toBe(0);
});
test('profile location choices survive a failed save on both create and edit', function (bool $creating) {
$group = ($this->kelompok)();
$url = $creating ? route('kelompok-tani.create') : route('kelompok-tani.edit', $group->id);
$input = ['provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1, 'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)'];
$response = $creating
? $this->from($url)->post(route('kelompok-tani.store'), $input)
: $this->from($url)->put(route('kelompok-tani.update', $group->id), $input);
$response->assertRedirect($url)->assertSessionHasErrors(['nama_responden', 'nama_kelompok']);
$page = $this->get($url)->assertOk();
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($page->getContent());
libxml_clear_errors();
$xpath = new DOMXPath($dom);
foreach (array_keys($input) as $field) {
expect($xpath->query('//select[@name="' . $field . '"]/option[@selected]')->length)->toBe(1);
}
expect($xpath->query('//select[@name="desa_kelurahan_id" and @disabled]')->length)->toBe(0);
})->with([true, false]);
test('invalid location input returns feedback without breaking the form reload', function ($invalidId) {
$url = route('kelompok-tani.create');
$this->from($url)->post(route('kelompok-tani.store'), [
'provinsi_id' => $invalidId, 'kabupaten_id' => $invalidId, 'kecamatan_id' => $invalidId, 'desa_kelurahan_id' => $invalidId,
])->assertRedirect($url)->assertSessionHasErrors(['provinsi_id', 'kabupaten_id', 'kecamatan_id', 'desa_kelurahan_id']);
$this->get($url)->assertOk()->assertSee('Pilihan Provinsi tidak valid.');
})->with([[['invalid']], ['1.5'], ['invalid']]);
test('aid experience and program entries are saved together in Operasional', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]);
$response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([
'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '1',
'bantuan' => [
['nama_program' => 'Bibit Kakao', 'penyelenggara' => 'Dinas Pertanian', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026],
['nama_program' => '', 'penyelenggara' => '', 'bentuk_bantuan' => '', 'tahun' => null],
],
]), $group);
expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeTrue();
expect($group->bantuan()->pluck('nama_program')->all())->toBe(['Bibit Kakao']);
expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-status-bantuan');
expect(session('success'))->toBe('Data Operasional berhasil disimpan.');
});
test('answering Belum Pernah ignores hidden program payloads and preserves existing history', function ($payload) {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]);
$group->bantuan()->create([
'uid' => 'BKT-TEST', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit',
]);
app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([
'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '0', 'bantuan' => $payload,
]), $group);
expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse();
expect($group->bantuan()->pluck('nama_program')->all())->toBe(['Program Lama']);
})->with([
['malformed'],
[[['nama_program' => ['invalid'], 'tahun' => 'invalid']]],
]);
test('invalid aid entries cannot partially update the experience answer', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]);
$request = ($this->profileRequest)([
'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '1',
'bantuan' => [['nama_program' => 'Program A', 'tahun' => date('Y') + 1]],
]);
expect(fn () => app(SurveyKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class);
expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse();
expect($group->bantuan()->count())->toBe(0);
});
test('aid creation failures roll back the experience answer', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]);
Event::listen('eloquent.creating: ' . BantuanKelompokTani::class, function () {
throw new RuntimeException('Simulated aid failure');
});
$request = ($this->profileRequest)([
'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '1',
'bantuan' => [['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]],
]);
expect(fn () => app(SurveyKelompokTaniController::class)->update($request, $group))->toThrow(RuntimeException::class);
expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse();
expect($group->bantuan()->count())->toBe(0);
});
test('aid list shares the experience box and is only visible for Pernah', function (?bool $savedAnswer, ?string $oldAnswer, bool $visible) {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => $savedAnswer]);
$request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET');
if ($oldAnswer !== null) {
session()->flashInput([
'pernah_mendapatkan_bantuan' => $oldAnswer,
'bantuan' => [['nama_program' => 'Program belum disimpan', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]],
]);
}
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group));
expect($dom->query('//form[@id="pengalaman-bantuan-form"]//div[@id="section-bantuan"]')->length)->toBe(1);
expect($dom->query('//div[@id="section-bantuan"]')->item(0)->hasAttribute('hidden'))->toBe(!$visible);
expect($dom->query('//fieldset[@id="bantuan-fields"]')->item(0)->hasAttribute('disabled'))->toBe(!$visible);
expect($dom->query('//form[@id="pengalaman-bantuan-form"]//button[contains(@class,"btn-success")]')->length)->toBe(1);
expect($dom->query('//form[@id="pengalaman-bantuan-form"]//form')->length)->toBe(0);
expect($dom->query('//div[@id="section-penilaian"] | //input[@name="potensi_kelompok"] | //textarea[@name="alasan_penilaian_potensi"]')->length)->toBe(0);
if ($oldAnswer !== null) {
expect($dom->query('//form[@id="pengalaman-bantuan-form"]//input[@value="Program belum disimpan"]')->length)->toBe(1);
}
})->with([
[null, null, false], [false, null, false], [true, null, true], [true, '0', false], [false, '1', true],
]);
test('old aid URLs redirect to Operasional and removed assessment fields cannot be saved', function () {
$group = ($this->kelompok)(['potensi_kelompok' => 'Rendah']);
$url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-status-bantuan';
expect(app(BantuanKelompokTaniController::class)->index($group)->getTargetUrl())->toBe($url);
$response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([
'section' => 'penilaian', 'potensi_kelompok' => 'Tinggi', 'alasan_penilaian_potensi' => 'Diabaikan',
]), $group);
expect($response->getTargetUrl())->toBe($url);
expect($group->refresh()->potensi_kelompok)->toBe('Rendah');
});
test('Pernah requires every first-row aid field before saving', function (string $missing) {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]);
$row = ['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026];
$row[$missing] = ' ';
$request = ($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1', 'bantuan' => [$row]]);
try {
app(BantuanKelompokTaniController::class)->update($request, $group);
$this->fail('An incomplete first row must be rejected.');
} catch (ValidationException $exception) {
expect($exception->errors())->toHaveKey('bantuan.0.' . $missing);
}
expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse();
expect($group->bantuan()->count())->toBe(0);
})->with(['nama_program', 'penyelenggara', 'bentuk_bantuan', 'tahun']);
test('Pernah rejects missing first aid rows including attempts to skip the first index', function ($rows) {
$group = ($this->kelompok)();
$request = ($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1', 'bantuan' => $rows]);
expect(fn () => app(BantuanKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class);
expect($group->bantuan()->count())->toBe(0);
})->with([
[null], [[]], [[[]]],
[[1 => ['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]]],
]);
test('recorded aid meets the minimum without requiring a duplicate entry', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]);
$group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']);
app(BantuanKelompokTaniController::class)->update(($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1']), $group);
expect($group->bantuan()->count())->toBe(1);
app(BantuanKelompokTaniController::class)->update(($this->profileRequest)([
'pernah_mendapatkan_bantuan' => '1', 'bantuan' => [['nama_program' => 'Program Tambahan'], []],
]), $group);
expect($group->bantuan()->count())->toBe(2);
});
test('an optional aid row with details still needs a program name', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]);
$group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']);
$request = ($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1', 'bantuan' => [['penyelenggara' => 'Dinas']]]);
expect(fn () => app(BantuanKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class);
expect($group->bantuan()->count())->toBe(1);
});
test('required aid markers only apply to the first unsaved row for Pernah', function (bool $pernah, bool $saved) {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => $pernah]);
if ($saved) {
$group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']);
}
$request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET');
session()->flashInput(['bantuan' => [[], []]]);
$dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group));
$expected = $pernah && !$saved ? 4 : 0;
expect($dom->query('//fieldset[@id="bantuan-fields"]//input[@required]')->length)->toBe($expected);
expect($dom->query('//fieldset[@id="bantuan-fields"]//span[contains(@class,"bantuan-required-mark") and not(@hidden)]')->length)->toBe($expected);
expect($dom->query('//div[@data-repeater-list="bantuan"]/div[2]//input[@required]')->length)->toBe(0);
expect($dom->query('//div[@id="section-bantuan"]/p[normalize-space()="Minimal satu data bantuan wajib diisi."]')->length)->toBe(1);
$firstRow = $saved
? '//div[@id="section-bantuan"]/div[1]'
: '//div[@data-repeater-list="bantuan"]/div[1]';
foreach (['Nama Program/Bantuan', 'Penyelenggara', 'Bentuk Bantuan', 'Tahun'] as $label) {
$marks = $dom->query($firstRow . '//label[starts-with(normalize-space(),"' . $label . '")]/span[contains(@class,"text-danger") and not(@hidden)]');
expect($marks->length)->toBe($saved || $pernah ? 1 : 0);
}
})->with([[true, false], [false, false], [true, true]]);
test('conflict topics are required for both Pernah answers without partial saves', function (string $answer, $topic) {
$group = ($this->kelompok)(['konflik_kelompok' => 'Tidak pernah', 'partisipasi_perempuan' => 'Hadir tapi pasif']);
$request = ($this->profileRequest)([
'section' => 'sosial-konflik', 'konflik_kelompok' => $answer,
'topik_konflik' => $topic, 'partisipasi_perempuan' => 'Tidak terlibat',
]);
try {
app(SurveyKelompokTaniController::class)->update($request, $group);
$this->fail('A conflict topic is required.');
} catch (ValidationException $exception) {
expect($exception->errors())->toHaveKey('topik_konflik');
}
expect($group->refresh()->konflik_kelompok)->toBe('Tidak pernah');
expect($group->partisipasi_perempuan)->toBe('Hadir tapi pasif');
})->with(['Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai'])->with([null, '', ' ']);
test('social saves accept conflict topics and require none for other answers', function ($answer, bool $required) {
$group = ($this->kelompok)();
$request = ($this->profileRequest)([
'section' => 'sosial-konflik', 'konflik_kelompok' => $answer,
...($required ? ['topik_konflik' => 'Pembagian hasil'] : []),
]);
app(SurveyKelompokTaniController::class)->update($request, $group);
expect($group->refresh()->topik_konflik)->toBe($required ? 'Pembagian hasil' : null);
expect(session('success'))->toBe('Data Sosial berhasil disimpan.');
($this->profileRequest)(['section' => 'sosial-konflik'], 'GET');
$dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.sosial-konflik', ['kelompokTani' => $group]));
$input = $dom->query('//input[@id="sk-topik-konflik"]')->item(0);
expect($input->hasAttribute('required'))->toBe($required);
expect($input->hasAttribute('disabled'))->toBe(!$required);
expect($dom->query('//label[@for="sk-topik-konflik"]/span[not(@hidden)]')->length)->toBe($required ? 1 : 0);
})->with([
['Pernah, sudah selesai', true], ['Pernah, masih berlanjut/belum selesai', true],
['Tidak pernah', false], ['Tidak bersedia menjawab', false], [null, false],
]);
test('conflict topic requirements and input follow the previous submission after validation', function () {
$group = ($this->kelompok)(['konflik_kelompok' => 'Tidak pernah']);
($this->profileRequest)([], 'GET');
session()->flashInput(['konflik_kelompok' => 'Pernah, sudah selesai', 'topik_konflik' => 'Pembagian hasil']);
$dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.sosial-konflik', ['kelompokTani' => $group]));
$input = $dom->query('//input[@id="sk-topik-konflik"]')->item(0);
expect($input->hasAttribute('required'))->toBeTrue();
expect($input->getAttribute('value'))->toBe('Pembagian hasil');
expect($dom->query('//div[@id="sk-detail-konflik-wrapper"]')->item(0)->hasAttribute('style'))->toBeFalse();
});
test('the last recorded aid cannot be deleted while the answer remains Pernah', function () {
$group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]);
$aid = $group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']);
$controller = app(BantuanKelompokTaniController::class);
$response = $controller->destroy($group, $aid);
expect($response->getTargetUrl())->toEndWith('#kelompok-tani-notifications');
expect($group->bantuan()->count())->toBe(1);
$other = $group->bantuan()->create(['uid' => 'BKT-OTHER', 'nama_program' => 'Program Lain', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']);
$controller->destroy($group, $aid);
expect($group->bantuan()->count())->toBe(1);
$group->update(['pernah_mendapatkan_bantuan' => false]);
$controller->destroy($group, $other);
expect($group->bantuan()->count())->toBe(0);
});
+338
View File
@@ -0,0 +1,338 @@
<?php
use App\Http\Controllers\Web\Dashboard\SummaryController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
Schema::create('master_komoditas', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->softDeletes();
});
foreach ([
'master_kabupaten_kota' => 'provinsi_id',
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->integer($parent);
});
}
Schema::create('petani', function (Blueprint $table) {
$table->id();
$table->integer('komoditas_id');
$table->integer('desa_kelurahan_id');
$table->softDeletes();
});
Schema::create('harga_jual_panen', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->string('bentuk_produk');
$table->integer('harga_panen_terakhir')->nullable();
$table->integer('harga_tahun_lalu')->nullable();
$table->integer('harga_dua_tahun_lalu')->nullable();
});
Schema::create('petani_skor', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->float('skor_kesiapan_adopsi');
$table->softDeletes();
});
Schema::create('akses_pasar', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->text('tujuan_penjualan')->nullable();
$table->text('sifat_hubungan_bisnis')->nullable();
$table->string('frekuensi_penjualan')->nullable();
$table->integer('skor_pengalaman')->nullable();
$table->integer('skor_kesadaran_premium')->nullable();
$table->softDeletes();
});
DB::table('master_komoditas')->insert([
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Vanili'],
['id' => 3, 'nama' => 'Kopi'], ['id' => 4, 'nama' => 'Kemiri'],
]);
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
DB::table('master_desa_kelurahan')->insert([
['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
]);
DB::table('petani')->insert([
['id' => 1, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 2, 'komoditas_id' => 1, 'desa_kelurahan_id' => 2, 'deleted_at' => null],
['id' => 3, 'komoditas_id' => 2, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 4, 'komoditas_id' => 3, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 5, 'komoditas_id' => 4, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 6, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => '2026-01-01'],
]);
foreach ([
[1, 'Biji kering/Biji Kupas', 10000], [2, 'Biji kering/Biji Kupas', 20000],
[1, 'Biji kering/Biji Kupas', 0], [1, 'Biji kering/Biji Kupas', null],
[1, 'Biji kering/Biji Kupas', -1000], [1, 'Biji basah', 90000],
[1, 'Buah segar hasil petik', 5000],
[3, 'Buah segar hasil petik', 30000], [3, 'Biji kering/Biji Kupas', 500000],
[3, 'Terfermentasi (Rp/kg)', 900000], [4, 'Buah segar hasil petik', 12000],
[5, 'Buah segar hasil petik', 8000], [5, 'Biji kering/Biji Kupas', 40000],
[6, 'Biji kering/Biji Kupas', 1000000],
] as [$farmer, $product, $price]) {
DB::table('harga_jual_panen')->insert([
'petani_id' => $farmer, 'bentuk_produk' => $product, 'harga_panen_terakhir' => $price,
]);
}
DB::table('petani_skor')->insert([
['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.2, 'deleted_at' => null],
['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.5, 'deleted_at' => null],
['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.5, 'deleted_at' => '2026-01-01'],
]);
$controller = app(SummaryController::class);
$method = new ReflectionMethod($controller, 'marketDashboardData');
$this->marketCards = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceCards'];
$this->marketCharts = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['marketCharts'];
$this->priceTrend = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceTrend'];
$this->premiumAwareness = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['premiumAwareness'];
});
test('market shows all four prices using the appropriate product and positive active farmer prices', function () {
$cards = ($this->marketCards)();
expect(array_column($cards, 'label'))->toBe([
'Kakao - Harga Biji Kering', 'Vanili - Harga Buah Segar',
'Kopi - Harga Ceri Merah', 'Kemiri - Harga Gelondong',
]);
expect(array_column($cards, 'value'))->toBe(['Rp 15.000', 'Rp 30.000', 'Rp 12.000', 'Rp 8.000']);
expect(array_column($cards, 'records'))->toBe([2, 1, 1, 1]);
});
test('market prices respect each region level and commodity selection', function () {
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
expect(($this->marketCards)([$region => 1])[0]['value'])
->toBe($region === 'desaKelurahan_id' ? 'Rp 10.000' : 'Rp 15.000');
expect(array_column(($this->marketCards)([$region => 99]), 'records'))->toBe([0, 0, 0, 0]);
}
$cards = ($this->marketCards)(['komoditas_id' => 2]);
expect(array_column($cards, 'records'))->toBe([0, 1, 0, 0]);
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 30.000', 'Rp 0', 'Rp 0']);
});
test('market prices respect segment selection and ignore deleted scores', function () {
$cards = ($this->marketCards)(['segmen' => 'Penggerak']);
expect(array_column($cards, 'records'))->toBe([1, 0, 0, 0]);
expect($cards[0]['value'])->toBe('Rp 10.000');
expect(($this->marketCards)(['segmen' => 'Pemerhati'])[0]['value'])->toBe('Rp 20.000');
});
test('market keeps four empty cards when no prices or active commodities are available', function () {
DB::table('master_komoditas')->update(['deleted_at' => '2026-01-01']);
$cards = ($this->marketCards)();
expect(array_column($cards, 'records'))->toBe([0, 0, 0, 0]);
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 0', 'Rp 0', 'Rp 0']);
});
test('market charts count multiple channels once per respondent and use answered question denominators', function () {
DB::table('akses_pasar')->insert([
[
'petani_id' => 1,
'tujuan_penjualan' => json_encode(['Koperasi', 'BUMDes', 'Koperasi', 'Lainnya']),
'sifat_hubungan_bisnis' => 'Bebas, bisa pindah pembeli kapan saja',
'frekuensi_penjualan' => 'Langsung setelah panen',
'skor_pengalaman' => 5,
],
[
'petani_id' => 2,
'tujuan_penjualan' => json_encode(['Koperasi']),
'sifat_hubungan_bisnis' => 'Titip jual,dibayar setelah produk terjual',
'frekuensi_penjualan' => null,
'skor_pengalaman' => 1,
],
[
'petani_id' => 3,
'tujuan_penjualan' => '[]',
'sifat_hubungan_bisnis' => '',
'frekuensi_penjualan' => 'Saat butuh uang mendesak',
'skor_pengalaman' => 3,
],
]);
$charts = ($this->marketCharts)();
expect($charts['saluranPenjualan']['series'])->toBe([0, 2, 1, 0, 1]);
expect($charts['saluranPenjualan']['respondents'])->toBe(2);
expect($charts['saluranPenjualan']['percentages'])->toBe([0.0, 100.0, 50.0, 0.0, 50.0]);
expect($charts['hubunganPembeli']['series'])->toBe([1, 0, 0, 1, 0, 0]);
expect($charts['hubunganPembeli']['respondents'])->toBe(2);
expect($charts['waktuPenjualan']['series'])->toBe([1, 0, 1]);
expect($charts['waktuPenjualan']['respondents'])->toBe(2);
expect($charts['aksesSaluranLain']['series'])->toBe([1, 0, 1, 0, 1]);
expect($charts['aksesSaluranLain']['percentages'])->toBe([33.3, 0.0, 33.3, 0.0, 33.3]);
});
test('market charts respect all dashboard filters and exclude deleted farmers and responses', function () {
foreach ([1, 2, 3, 6] as $farmer) {
DB::table('akses_pasar')->insert([
'petani_id' => $farmer,
'tujuan_penjualan' => json_encode(['Koperasi']),
'sifat_hubungan_bisnis' => 'Langganan tetap, tapi tidak terikat',
'frekuensi_penjualan' => 'Ditumpuk dulu beberapa waktu',
'skor_pengalaman' => 4,
]);
}
DB::table('akses_pasar')->insert([
'petani_id' => 4, 'tujuan_penjualan' => json_encode(['BUMDes']), 'deleted_at' => '2026-01-01',
]);
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
foreach (($this->marketCharts)([$region => 1]) as $chart) {
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 2 : 3);
}
foreach (($this->marketCharts)([$region => 99]) as $chart) {
expect($chart['respondents'])->toBe(0);
}
}
foreach ([['komoditas_id' => 1], ['komoditas_id' => 2], ['segmen' => 'Penggerak'], ['segmen' => 'Pemerhati']] as $filters) {
foreach (($this->marketCharts)($filters) as $chart) {
expect($chart['respondents'])->toBe($filters === ['komoditas_id' => 1] ? 2 : 1);
}
}
foreach (($this->marketCharts)(['komoditas_id' => 2, 'segmen' => 'Penggerak']) as $chart) {
expect($chart['respondents'])->toBe(0);
}
});
test('market charts keep ordered zero categories when answers are missing or invalid', function () {
DB::table('akses_pasar')->insert([
'petani_id' => 1, 'tujuan_penjualan' => '[null, {}, "Unknown"]',
'sifat_hubungan_bisnis' => 'Unknown', 'frekuensi_penjualan' => '', 'skor_pengalaman' => 9,
]);
$charts = ($this->marketCharts)();
expect(array_map(fn ($chart) => count($chart['series']), $charts))->toBe([
'saluranPenjualan' => 5, 'hubunganPembeli' => 6, 'waktuPenjualan' => 3, 'aksesSaluranLain' => 5,
]);
foreach ($charts as $chart) {
expect($chart['respondents'])->toBe(0);
expect(array_sum($chart['series']))->toBe(0);
expect(array_sum($chart['percentages']))->toBe(0);
}
});
test('price trend averages each product and commodity chronologically without treating missing prices as zero', function () {
DB::table('harga_jual_panen')->delete();
foreach ([
[1, 'Buah segar hasil petik', 10000, 8000, 6000],
[2, 'Buah segar hasil petik', 20000, 12000, null],
[3, 'Buah segar hasil petik', 30000, 20000, 10000],
[1, 'Biji basah', 0, 7000, 4000],
[1, 'Biji kering/Biji Kupas', -1000, null, 0],
[1, 'Terfermentasi (Rp/kg)', null, 20000, 10000],
[6, 'Buah segar hasil petik', 1000000, 1000000, 1000000],
] as [$farmer, $product, $latest, $lastYear, $twoYearsAgo]) {
DB::table('harga_jual_panen')->insert([
'petani_id' => $farmer, 'bentuk_produk' => $product,
'harga_panen_terakhir' => $latest, 'harga_tahun_lalu' => $lastYear, 'harga_dua_tahun_lalu' => $twoYearsAgo,
]);
}
$trend = ($this->priceTrend)();
$commodities = array_column($trend['commodities'], null, 'id');
expect($trend['periods'])->toBe(['Dua Tahun Lalu', 'Tahun Lalu', 'Panen Terakhir']);
expect(array_column($commodities[1]['series'], 'name'))->toBe([
'Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi',
]);
expect(array_column($commodities[1]['series'], 'data'))->toBe([
[6000.0, 10000.0, 15000.0], [4000.0, 7000.0, null], [null, null, null], [10000.0, 20000.0, null],
]);
expect($commodities[2]['series'][0]['data'])->toBe([10000.0, 20000.0, 30000.0]);
});
test('price trend respects dashboard region commodity and segment filters', function () {
// Historical prices must follow the same filters as the latest harvest.
DB::table('harga_jual_panen')->where('harga_panen_terakhir', '>', 0)
->update(['harga_tahun_lalu' => DB::raw('harga_panen_terakhir / 2')]);
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
$commodities = array_column(($this->priceTrend)([$region => 1])['commodities'], null, 'id');
$price = $region === 'desaKelurahan_id' ? 10000.0 : 15000.0;
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
foreach (($this->priceTrend)([$region => 99])['commodities'] as $commodity) {
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
}
}
$trend = ($this->priceTrend)(['komoditas_id' => 2]);
expect(array_column($trend['commodities'], 'id'))->toBe([2]);
expect($trend['commodities'][0]['series'][0]['data'])->toBe([null, 15000.0, 30000.0]);
foreach (['Penggerak' => 10000.0, 'Pemerhati' => 20000.0] as $segment => $price) {
$commodities = array_column(($this->priceTrend)(['segmen' => $segment])['commodities'], null, 'id');
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
expect($commodities[2]['series'][0]['data'])->toBe([null, null, null]);
}
});
test('price trend retains missing points and handles empty or deleted commodity selections', function () {
DB::table('harga_jual_panen')->delete();
$trend = ($this->priceTrend)();
expect($trend['commodities'])->toHaveCount(4);
foreach ($trend['commodities'] as $commodity) {
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
}
expect(($this->priceTrend)(['komoditas_id' => 99])['commodities'])->toBe([]);
DB::table('master_komoditas')->where('id', 1)->update(['deleted_at' => '2026-01-01']);
expect(($this->priceTrend)(['komoditas_id' => 1])['commodities'])->toBe([]);
});
test('premium awareness counts each active farmer once and preserves the five ordered levels', function () {
DB::table('akses_pasar')->insert([
'petani_id' => 4, 'skor_kesadaran_premium' => 4, 'deleted_at' => '2026-01-01',
]);
foreach ([1 => 1, 2 => 3, 3 => 3, 4 => 5, 5 => null, 6 => 5] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
DB::table('akses_pasar')->insert(['petani_id' => 1, 'skor_kesadaran_premium' => 2]);
$chart = ($this->premiumAwareness)();
expect($chart['series'])->toBe([1, 0, 2, 0, 1]);
expect($chart['percentages'])->toBe([25.0, 0.0, 50.0, 0.0, 25.0]);
expect($chart['respondents'])->toBe(4);
expect($chart['fullLabels'][0])->toBe('Belum pernah mendengar sama sekali');
expect($chart['fullLabels'][4])->toBe('Sudah mengakses dan mendapat harga premium secara konsisten');
});
test('premium awareness respects all dashboard region commodity and segment filters', function () {
foreach ([1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 5] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
$chart = ($this->premiumAwareness)([$region => 1]);
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 4 : 5);
expect(($this->premiumAwareness)([$region => 99])['respondents'])->toBe(0);
}
expect(($this->premiumAwareness)(['komoditas_id' => 1])['series'])->toBe([1, 1, 0, 0, 0]);
expect(($this->premiumAwareness)(['komoditas_id' => 2])['series'])->toBe([0, 0, 1, 0, 0]);
expect(($this->premiumAwareness)(['segmen' => 'Penggerak'])['series'])->toBe([1, 0, 0, 0, 0]);
expect(($this->premiumAwareness)(['segmen' => 'Pemerhati'])['series'])->toBe([0, 1, 0, 0, 0]);
expect(($this->premiumAwareness)(['komoditas_id' => 2, 'segmen' => 'Penggerak'])['respondents'])->toBe(0);
});
test('premium awareness retains empty categories and excludes missing or invalid scores', function () {
$emptyChart = ($this->premiumAwareness)();
foreach ([1 => 0, 2 => 6, 3 => null, 4 => -1, 5 => null] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
$chart = ($this->premiumAwareness)();
expect($chart)->toBe($emptyChart);
expect($chart['series'])->toBe([0, 0, 0, 0, 0]);
expect($chart['respondents'])->toBe(0);
expect($chart['labels'])->toHaveCount(5);
expect(array_sum($chart['percentages']))->toBe(0);
});