Update Pendidikan Pelatihan

This commit is contained in:
2026-07-11 15:19:44 +07:00
parent df5bfeb67f
commit d33bc8a42b
7 changed files with 768 additions and 7 deletions
@@ -5,20 +5,159 @@ namespace App\Http\Controllers\Web\Produktivitas;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use App\Models\Pertanian\Petani;
use App\Models\Pertanian\SurveyPelatihan;
use App\Models\Pertanian\SurveySertifikasi;
class PendidikanPelatihanController extends Controller
{
public function index($petaniId, Request $request)
private function authorizeAccess(): void
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
}
public function index(Petani $petani)
{
$this->authorizeAccess();
$title = 'Petani - Pendidikan dan Pelatihan';
$petani = Petani::findOrFail($petaniId);
return view('produktivitas.petani.pendidikan-pelatihan.add', compact('title', 'petani'));
$petani->load([
'surveyPelatihan',
'surveySertifikasi',
]);
return view('produktivitas.petani.pendidikan-pelatihan.index', compact(
'title',
'petani'
));
}
public function storePelatihan(Request $request, Petani $petani)
{
$this->authorizeAccess();
$validatedData = $request->validate([
'riwayat_pelatihan' => ['nullable', 'array'],
'riwayat_pelatihan.*.jenis_pelatihan' => ['nullable', 'string', 'max:255'],
'riwayat_pelatihan.*.penyelenggara' => ['nullable', 'string', 'max:255'],
'riwayat_pelatihan.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
], [
'riwayat_pelatihan.*.tahun.integer' => 'Tahun pelatihan harus berupa angka.',
'riwayat_pelatihan.*.tahun.min' => 'Tahun pelatihan tidak valid.',
'riwayat_pelatihan.*.tahun.max' => 'Tahun pelatihan tidak boleh melebihi tahun saat ini.',
], [
'riwayat_pelatihan.*.jenis_pelatihan' => 'jenis pelatihan',
'riwayat_pelatihan.*.penyelenggara' => 'penyelenggara',
'riwayat_pelatihan.*.tahun' => 'tahun pelatihan',
]);
foreach ($validatedData['riwayat_pelatihan'] ?? [] as $item) {
if (
empty($item['jenis_pelatihan']) &&
empty($item['penyelenggara']) &&
empty($item['tahun'])
) {
continue;
}
$petani->surveyPelatihan()->create([
'uid' => 'SPL-' . strtoupper(Str::random(10)),
'petani_id' => $petani->id,
'jenis_pelatihan' => $item['jenis_pelatihan'] ?? null,
'penyelenggara' => $item['penyelenggara'] ?? null,
'tahun_pelaksanaan' => $item['tahun'] ?? null,
]);
}
return back()->with('success', 'Riwayat pelatihan berhasil disimpan.');
}
public function destroyPelatihan(Petani $petani, SurveyPelatihan $surveyPelatihan)
{
$this->authorizeAccess();
abort_unless($surveyPelatihan->petani_id === $petani->id, 404);
$surveyPelatihan->delete();
return back()->with('success', 'Riwayat pelatihan berhasil dihapus.');
}
public function storeSertifikasi(Request $request, Petani $petani)
{
$this->authorizeAccess();
$sertifikasi = $request->input('sertifikasi', []);
foreach ($sertifikasi as $index => $item) {
$status = $item['status_berlaku_sertifikat'] ?? 0;
if (is_array($status)) {
$status = in_array('1', $status, true) ? 1 : 0;
} else {
$status = $status == 1 ? 1 : 0;
}
$sertifikasi[$index]['status_berlaku_sertifikat'] = $status;
}
$request->merge([
'sertifikasi' => $sertifikasi,
]);
$validatedData = $request->validate([
'sertifikasi' => ['nullable', 'array'],
'sertifikasi.*.jenis_sertifikasi' => ['nullable', 'string', 'max:255'],
'sertifikasi.*.lembaga_penerbit' => ['nullable', 'string', 'max:255'],
'sertifikasi.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
'sertifikasi.*.status_berlaku_sertifikat' => ['nullable', 'boolean'],
], [
'sertifikasi.*.tahun.integer' => 'Tahun sertifikasi harus berupa angka.',
'sertifikasi.*.tahun.min' => 'Tahun sertifikasi tidak valid.',
'sertifikasi.*.tahun.max' => 'Tahun sertifikasi tidak boleh melebihi tahun saat ini.',
], [
'sertifikasi.*.jenis_sertifikasi' => 'jenis sertifikasi',
'sertifikasi.*.lembaga_penerbit' => 'lembaga penerbit',
'sertifikasi.*.tahun' => 'tahun sertifikasi',
'sertifikasi.*.status_berlaku_sertifikat' => 'status sertifikat',
]);
foreach ($validatedData['sertifikasi'] ?? [] as $item) {
if (
empty($item['jenis_sertifikasi']) &&
empty($item['lembaga_penerbit']) &&
empty($item['tahun'])
) {
continue;
}
$petani->surveySertifikasi()->create([
'uid' => 'SPS-' . strtoupper(Str::random(10)),
'petani_id' => $petani->id,
'jenis_sertifikasi' => $item['jenis_sertifikasi'] ?? null,
'lembaga_penerbit' => $item['lembaga_penerbit'] ?? null,
'tahun_pelaksanaan' => $item['tahun'] ?? null,
'is_aktif' => $item['status_berlaku_sertifikat'] ?? 0,
]);
}
return back()->with('success', 'Sertifikasi berhasil disimpan.');
}
public function destroySertifikasi(Petani $petani, SurveySertifikasi $surveySertifikasi)
{
$this->authorizeAccess();
abort_unless($surveySertifikasi->petani_id === $petani->id, 404);
$surveySertifikasi->delete();
return back()->with('success', 'Sertifikasi berhasil dihapus.');
}
}
+10
View File
@@ -108,6 +108,16 @@ class Petani extends Model
return $this->hasOne(surveyPanen::class, 'petani_id')->latestOfMany();
}
public function surveyPelatihan(): HasMany
{
return $this->hasMany(surveyPelatihan::class, 'petani_id');
}
public function surveySertifikasi(): HasMany
{
return $this->hasMany(surveySertifikasi::class, 'petani_id');
}
public function surveyProduktivitas(): HasOne
{
return $this->hasOne(surveyProduktivitas::class, 'petani_id')->latestOfMany();
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models\Pertanian;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\Pertanian\Petani;
class SurveyPelatihan extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'survey_pelatihan';
protected $fillable = [
'uid',
'petani_id',
'jenis_pelatihan',
'penyelenggara',
'tahun_pelaksanaan',
'created_by',
'updated_by',
'deleted_by',
];
public function petani(): BelongsTo
{
return $this->belongsTo(Petani::class, 'petani_id');
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Models\Pertanian;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SurveySertifikasi extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'survey_sertifikasi';
protected $fillable = [
'uid',
'petani_id',
'jenis_sertifikasi',
'lembaga_penerbit',
'tahun_pelaksanaan',
'is_aktif',
'created_by',
'updated_by',
'deleted_by',
];
public function petani()
{
return $this->belongsTo(Petani::class, 'petani_id');
}
}
@@ -285,7 +285,7 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('petani.edit', $petani->id) }}">
<a class="nav-link {{ request()->routeIs('petani.pendidikan-pelatihan.*') ? 'active' : '' }}" href="{{ route('petani.pendidikan-pelatihan.index', $petani->id) }}">
<i class="icon-base ti tabler-books me-1_5 icon-sm"></i>Pendidikan & Pelatihan
</a>
</li>
@@ -0,0 +1,539 @@
@extends('layout.main')
@push('style')
<style>
.status-switch-row {
display: flex !important;
align-items: center !important;
gap: 12px !important;
white-space: nowrap !important;
}
.status-switch-row .switch {
width: 52px !important;
min-width: 52px !important;
max-width: 52px !important;
padding-left: 0 !important;
margin: 0 !important;
position: relative !important;
display: inline-block !important;
flex: 0 0 52px !important;
}
.status-switch-row .switch-toggle-slider {
left: 0 !important;
}
.status-switch-row .status-text {
display: inline-block !important;
white-space: nowrap !important;
line-height: 1.5 !important;
}
</style>
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="row">
<!-- PROFIL PETANI -->
<div class="col-md-12">
@include('produktivitas.petani.header', ['petani' => $petani])
<div class="card mb-6">
<div class="card-body pt-12">
<h5 class="pb-4 border-bottom mb-4">
Riwayat Pelatihan
</h5>
{{-- DATA LAMA --}}
@if ($petani->surveyPelatihan->count() > 0)
@foreach ($petani->surveyPelatihan as $row)
<div class="row align-items-end border-bottom pb-3 mb-3">
<div class="col-md-5">
<label class="form-label">Jenis Pelatihan</label>
<input type="text" class="form-control" value="{{ $row->jenis_pelatihan }}" disabled>
</div>
<div class="col-md-4">
<label class="form-label">Penyelenggara</label>
<input type="text" class="form-control" value="{{ $row->penyelenggara }}" disabled>
</div>
<div class="col-md-2">
<label class="form-label">Tahun</label>
<input type="text" class="form-control" value="{{ $row->tahun_pelaksanaan }}" disabled>
</div>
<div class="col-md-1">
<form
method="POST"
action="{{ route('petani.pendidikan-pelatihan.pelatihan.destroy', [$petani->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 riwayat pelatihan.</p>
@endif
{{-- FORM TAMBAH DATA BARU --}}
<form
method="POST"
action="{{ route('petani.pendidikan-pelatihan.pelatihan.store', $petani->id) }}"
class="form-repeater-pelatihan">
@csrf
<div data-repeater-list="riwayat_pelatihan">
<div data-repeater-item>
<div class="row align-items-end">
<div class="col-md-5 mb-3">
<label class="form-label">Jenis Pelatihan</label>
<input
type="text"
class="form-control pelatihan-input"
name="jenis_pelatihan"
placeholder="Contoh: Pelatihan GAP">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Penyelenggara</label>
<input
type="text"
class="form-control pelatihan-input"
name="penyelenggara"
placeholder="Contoh: Dinas Pertanian">
</div>
<div class="col-md-2 mb-3">
<label class="form-label d-block">Tahun</label>
<input
type="number"
class="form-control pelatihan-input"
name="tahun"
placeholder="Contoh: 2024"
min="1900"
max="{{ date('Y') }}">
</div>
<div class="col-md-1 mb-3">
<button
type="button"
class="btn btn-outline-danger btn-delete-pelatihan"
data-repeater-delete
disabled>
<i class="icon-base ti tabler-trash icon-sm"></i>
</button>
</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-pelatihan"
data-repeater-create
disabled>
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i>
Tambah Pelatihan
</button>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan Riwayat Pelatihan
</button>
</div>
</form>
</div>
</div>
{{-- SERTIFIKASI KEAHLIAN --}}
<div class="card mb-6">
<div class="card-body pt-12">
<h5 class="pb-4 border-bottom mb-4">
Sertifikasi Keahlian
</h5>
{{-- DATA LAMA --}}
@if ($petani->surveySertifikasi->count() > 0)
@foreach ($petani->surveySertifikasi as $row)
<div class="row align-items-end border-bottom pb-3 mb-3">
<div class="col-md-4">
<label class="form-label">Jenis Keahlian Yang Disertifikasi</label>
<input type="text" class="form-control" value="{{ $row->jenis_sertifikasi }}" disabled>
</div>
<div class="col-md-3">
<label class="form-label">Lembaga Penerbit</label>
<input type="text" class="form-control" value="{{ $row->lembaga_penerbit }}" disabled>
</div>
<div class="col-md-2">
<label class="form-label">Tahun</label>
<input type="text" class="form-control" value="{{ $row->tahun_pelaksanaan }}" disabled>
</div>
<div class="col-md-2">
<label class="form-label d-block mb-1">Status</label>
@php
$statusBerlaku = (bool) $row->is_aktif;
@endphp
<div class="status-switch-row">
<label class="switch switch-lg">
<input
type="checkbox"
class="switch-input"
{{ $statusBerlaku ? 'checked' : '' }}
onclick="return false;"
tabindex="-1">
<span class="switch-toggle-slider">
<span class="switch-on">
<i class="icon-base ti tabler-check"></i>
</span>
<span class="switch-off">
<i class="icon-base ti tabler-x"></i>
</span>
</span>
</label>
<span class="status-text">
{{ $statusBerlaku ? 'Masih Berlaku' : 'Tidak Berlaku' }}
</span>
</div>
</div>
<div class="col-md-1">
<form
method="POST"
action="{{ route('petani.pendidikan-pelatihan.sertifikasi.destroy', [$petani->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 data sertifikasi.</p>
@endif
{{-- FORM TAMBAH DATA BARU --}}
<form
method="POST"
action="{{ route('petani.pendidikan-pelatihan.sertifikasi.store', $petani->id) }}"
class="form-repeater-sertifikasi">
@csrf
<div data-repeater-list="sertifikasi">
<div data-repeater-item>
<div class="row align-items-end">
<div class="col-md-4 mb-3">
<label class="form-label">Jenis Keahlian Yang Disertifikasi</label>
<input
type="text"
class="form-control sertifikasi-input"
name="jenis_sertifikasi"
placeholder="Contoh: Sertifikasi GAP">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Lembaga Penerbit</label>
<input
type="text"
class="form-control sertifikasi-input"
name="lembaga_penerbit"
placeholder="Contoh: BNSP">
</div>
<div class="col-md-2 mb-3">
<label class="form-label d-block">Tahun</label>
<input
type="number"
class="form-control sertifikasi-input"
name="tahun"
placeholder="Contoh: 2024"
min="1900"
max="{{ date('Y') }}">
</div>
<div class="col-md-2 mb-3">
<label class="form-label d-block">Status</label>
<label class="switch switch-lg">
<input type="hidden" name="status_berlaku_sertifikat" value="0">
<input
type="checkbox"
class="switch-input sertifikasi-status-switch"
name="status_berlaku_sertifikat"
value="1">
<span class="switch-toggle-slider">
<span class="switch-on">
<i class="icon-base ti tabler-check"></i>
</span>
<span class="switch-off">
<i class="icon-base ti tabler-x"></i>
</span>
</span>
<span class="switch-label">
<span class="sertifikasi-switch-text">Tidak Berlaku</span>
</span>
</label>
</div>
<div class="col-md-1 mb-3">
<button
type="button"
class="btn btn-outline-danger btn-delete-sertifikasi"
data-repeater-delete
disabled>
<i class="icon-base ti tabler-trash icon-sm"></i>
</button>
</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-sertifikasi"
data-repeater-create
disabled>
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i>
Tambah Sertifikasi
</button>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan Sertifikasi Keahlian
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
'use strict';
$(function () {
function initRepeaterWithValidation(config) {
const $form = $(config.formSelector);
const $btnTambah = $(config.addButtonSelector);
if (!$form.length || !$btnTambah.length) {
return;
}
function getRows() {
return $form.find('[data-repeater-item]');
}
function rowAdaIsi($row) {
return $row.find(config.inputSelector).toArray().some(function (input) {
return String($(input).val() || '').trim() !== '';
});
}
function rowLengkap($row) {
const $inputs = $row.find(config.inputSelector);
if ($inputs.length === 0) {
return false;
}
return $inputs.toArray().every(function (input) {
return String($(input).val() || '').trim() !== '';
});
}
function updateSwitchText($switch) {
if (!config.switchSelector || !config.switchTextSelector) {
return;
}
const $row = $switch.closest('[data-repeater-item]');
const $text = $row.find(config.switchTextSelector);
if ($switch.is(':checked')) {
$text.text(config.switchTextOn || 'Ya');
} else {
$text.text(config.switchTextOff || 'Tidak');
}
}
function updateAllSwitchText() {
if (!config.switchSelector) {
return;
}
$form.find(config.switchSelector).each(function () {
updateSwitchText($(this));
});
}
function updateButtons() {
const $rows = getRows();
const $lastRow = $rows.last();
$rows.each(function () {
const $row = $(this);
const $deleteButton = $row.find(config.deleteButtonSelector);
$deleteButton.prop('disabled', !rowAdaIsi($row));
});
$btnTambah.prop('disabled', !rowLengkap($lastRow));
}
$form.repeater({
initEmpty: false,
show: function () {
const $row = $(this);
$row.find(config.inputSelector).val('');
$row.find(config.deleteButtonSelector).prop('disabled', true);
if (config.switchSelector) {
$row.find(config.switchSelector).prop('checked', false);
}
updateAllSwitchText();
$row.slideDown();
setTimeout(updateButtons, 50);
},
hide: function (deleteElement) {
const $row = $(this);
// Kalau baris kosong, jangan lakukan apa-apa
if (!rowAdaIsi($row)) {
return;
}
// Ini hanya hapus baris di form, tidak kirim ke DB
Swal.fire({
title: config.deleteTitle || 'Hapus baris ini?',
text: 'Baris ini hanya akan dihapus dari form.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, hapus',
cancelButtonText: 'Batal',
customClass: {
confirmButton: 'btn btn-danger me-3',
cancelButton: 'btn btn-label-secondary'
},
buttonsStyling: false
}).then(function (result) {
if (result.isConfirmed) {
$row.slideUp(deleteElement);
setTimeout(updateButtons, 300);
}
});
}
});
$(document).on('input keyup change', config.formSelector + ' ' + config.inputSelector, function () {
updateButtons();
});
if (config.switchSelector) {
$(document).on('change', config.formSelector + ' ' + config.switchSelector, function () {
updateSwitchText($(this));
updateButtons();
});
}
updateAllSwitchText();
updateButtons();
}
initRepeaterWithValidation({
formSelector: '.form-repeater-pelatihan',
addButtonSelector: '#btn-tambah-pelatihan',
inputSelector: '.pelatihan-input',
deleteButtonSelector: '.btn-delete-pelatihan',
deleteTitle: 'Hapus baris pelatihan ini?'
});
initRepeaterWithValidation({
formSelector: '.form-repeater-sertifikasi',
addButtonSelector: '#btn-tambah-sertifikasi',
inputSelector: '.sertifikasi-input',
deleteButtonSelector: '.btn-delete-sertifikasi',
deleteTitle: 'Hapus baris sertifikasi ini?',
switchSelector: '.sertifikasi-status-switch',
switchTextSelector: '.sertifikasi-switch-text',
switchTextOn: 'Masih Berlaku',
switchTextOff: 'Tidak Berlaku'
});
// Ini khusus untuk data yang SUDAH ADA DI DATABASE
$(document).on('submit', '.delete-db-form', function (e) {
e.preventDefault();
const form = this;
Swal.fire({
title: 'Hapus data ini?',
text: 'Data yang sudah dihapus tidak bisa dikembalikan.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, hapus',
cancelButtonText: 'Batal',
customClass: {
confirmButton: 'btn btn-danger me-3',
cancelButton: 'btn btn-label-secondary'
},
buttonsStyling: false
}).then(function (result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
</script>
@endpush
+9 -1
View File
@@ -197,7 +197,15 @@ Route::middleware('auth')
Route::resource('petani.lahan-tanaman.tanaman', App\Http\Controllers\Web\Produktivitas\PetaniLahanTanamanController::class)->only(['show', 'update', 'destroy'])->whereNumber('petani');
Route::resource('petani.praktik-budidaya', App\Http\Controllers\Web\Produktivitas\PraktikBudidayaController::class)->whereNumber('petani');
Route::resource('petani.survey-produktivitas', App\Http\Controllers\Web\Produktivitas\SurveyProduktivitasController::class)->only(['store', 'destroy']);
Route::resource('petani.pendidikan-pelatihan', App\Http\Controllers\Web\Produktivitas\PendidikanPelatihanController::class);
Route::prefix('petani/{petani}/pendidikan-pelatihan')->name('petani.pendidikan-pelatihan.')->group(function () {
Route::get('/', [App\Http\Controllers\Web\Produktivitas\PendidikanPelatihanController::class, 'index'])->name('index');
Route::post('/pelatihan', [App\Http\Controllers\Web\Produktivitas\PendidikanPelatihanController::class, 'storePelatihan'])->name('pelatihan.store');
Route::delete('/pelatihan/{surveyPelatihan}', [App\Http\Controllers\Web\Produktivitas\PendidikanPelatihanController::class, 'destroyPelatihan'])->name('pelatihan.destroy');
Route::post('/sertifikasi', [App\Http\Controllers\Web\Produktivitas\PendidikanPelatihanController::class, 'storeSertifikasi'])->name('sertifikasi.store');
Route::delete('/sertifikasi/{surveySertifikasi}', [App\Http\Controllers\Web\Produktivitas\PendidikanPelatihanController::class, 'destroySertifikasi'])->name('sertifikasi.destroy');
});
Route::resource('petani.sosial-ekonomi', App\Http\Controllers\Web\Produktivitas\SosialEkonomiController::class)->whereNumber('petani');
Route::resource('petani.akses-pasar', App\Http\Controllers\Web\Produktivitas\AksesPasarController::class)->whereNumber('petani');
});