[Add] Menambah fitur Data Sarana#1335
[Add] Menambah fitur Data Sarana#1335andikachamberlin wants to merge 3 commits intoOpenSID:masterfrom
Conversation
|
@andikachamberlin untuk proses input data sarana perlu ada perubahan :
|
|
/review |
|
/review |
🔒 Security ReviewTotal Temuan: 8 isu (3 Critical, 4 High, 1 Medium)
|
⚡ Performance ReviewTotal Temuan: 5 isu (2 Critical, 3 High)
|
| ->addColumn('desa', function ($row) { | ||
| return $row->desa ? $row->desa->nama : '-'; | ||
| }) | ||
| ->addColumn('aksi', function ($row) { |
There was a problem hiding this comment.
[CRITICAL] ⚡ Performance: N+1 Query di DataTables
Masalah: Method getData() tidak melakukan eager loading relasi desa, menyebabkan N+1 queries saat render DataTables dengan banyak rows.
Kode: $query = DataSarana::query();
Dampak: Pada list dengan 100 sarana, akan terjadi 1 query utama + 100 query untuk load relasi desa = 101 queries total. Di production dengan traffic tinggi, ini bisa menyebabkan database bottleneck.
Fix:
// Line 42 - Tambahkan eager loading
$query = DataSarana::with('desa:id,nama')->query();
// Atau jika perlu filter desa
if ($request->has('desa_id') && $request->desa_id != '') {
$query->where('desa_id', $request->desa_id);
}| protected $author; | ||
| protected $data; | ||
|
|
||
| public function __construct(Collection $data, $author = 'Admin') |
There was a problem hiding this comment.
[CRITICAL] ⚡ Performance: Memory Overflow pada Export Besar
Masalah: Method collection() load semua data sekaligus ke memory tanpa chunking. Untuk dataset besar (>10k rows), ini akan menyebabkan memory exhaustion.
Kode: return DataSarana::with('desa')->get();
Dampak: Export 50k sarana dengan relasi desa bisa consume 500MB+ memory. Server dengan memory_limit 256M akan crash dengan error "Allowed memory size exhausted".
Fix:
// Ganti FromCollection dengan FromQuery + chunking
use Maatwebsite\Excel\Concerns\FromQuery;
class ExportDataSarana implements FromQuery, WithHeadings, WithStyles, WithEvents
{
public function query()
{
return DataSarana::query()->with('desa:id,nama');
}
// Maatwebsite Excel akan otomatis chunk query ini
// Atau gunakan cursor() untuk memory efficiency
}| @@ -51,7 +52,12 @@ public function index() | |||
| $page_title = 'Data Umum'; | |||
There was a problem hiding this comment.
[HIGH] ⚡ Performance: Missing Cache untuk Data Statis
Masalah: Query agregasi sarana dipanggil setiap kali form edit dibuka, padahal data ini relatif statis dan bisa di-cache.
Kode: $pengaturan = PpidPengaturan::first(); dan 13 query SUM untuk setiap kategori sarana
Dampak: Setiap page load form edit = 14 queries (1 untuk pengaturan + 13 untuk rekap sarana). Dengan 1000 page views/hari = 14,000 queries yang sebenarnya bisa di-cache.
Fix:
// Cache rekap sarana per desa selama 1 jam
$rekapSarana = Cache::remember("sarana_rekap_{$desa_id}", 3600, function() use ($desa_id) {
return [
'puskesmas' => DataSarana::where('desa_id', $desa_id)
->where('kategori', 'puskesmas')->sum('jumlah'),
'puskesmas_pembantu' => DataSarana::where('desa_id', $desa_id)
->where('kategori', 'puskesmas_pembantu')->sum('jumlah'),
// ... dst untuk 13 kategori
];
});
// Invalidate cache saat ada perubahan di DataSaranaController:
// Cache::forget("sarana_rekap_{$request->desa_id}");| */ | ||
| public function model(array $row) | ||
| { | ||
| if (!DataDesa::where('id', $row['desa_id'])->exists()) { |
There was a problem hiding this comment.
[HIGH] ⚡ Performance: Query Validasi di Loop Import
Masalah: Setiap row import melakukan query DataDesa::where('id', $row['desa_id'])->exists() secara individual, menyebabkan N queries untuk N rows.
Kode: if (!DataDesa::where('id', $row['desa_id'])->exists()) { return null; }
Dampak: Import 1000 rows = 1000 queries validasi. Proses import yang seharusnya 5 detik bisa jadi 30+ detik karena database round-trip overhead.
Fix:
// Preload semua valid desa_id sekali di constructor
private $validDesaIds;
public function __construct()
{
$this->validDesaIds = DataDesa::pluck('id')->flip();
}
public function model(array $row)
{
// Validasi menggunakan in-memory array
if (!isset($this->validDesaIds[$row['desa_id']])) {
\Log::warning("Import skipped: desa_id {$row['desa_id']} not found");
return null;
}
return new DataSarana([
'desa_id' => $row['desa_id'],
'nama' => $row['nama'],
'jumlah' => $row['jumlah'],
'kategori' => $row['kategori'],
'keterangan' => $row['keterangan'] ?? null,
]);
}| </div> | ||
| </div> | ||
| </div> | ||
| </section> |
There was a problem hiding this comment.
[HIGH] ⚡ Performance: jQuery Selector Tidak Di-cache
Masalah: Selector $('#table') dipanggil setiap kali DataTables event fire, tanpa caching. Pada table dengan banyak interaksi (sort, filter, pagination), ini menyebabkan DOM query berulang.
Kode: $('#table').DataTable({ ... })
Dampak: Setiap user interaction (sort/filter) = re-query DOM. Pada table dengan 1000+ rows dan frequent interaction, ini bisa menyebabkan UI lag 100-200ms per action.
Fix:
// Cache selector di awal
$(document).ready(function() {
var $table = $('#table'); // Cache sekali
$table.DataTable({
processing: true,
serverSide: true,
ajax: {
url: "{{ route('data.data-sarana.getdata') }}",
data: function(d) {
d.desa_id = $('#desa_id').val(); // Cache ini juga
d.kategori = $('#kategori').val();
}
},
// ... rest of config
});
// Cache selector untuk filter juga
var $desaFilter = $('#desa_id');
var $kategoriFilter = $('#kategori');
$desaFilter.on('change', function() {
$table.DataTable().ajax.reload();
});
});
📝 Code Quality ReviewTotal Temuan: 8 isu (5 Critical, 3 High)
|
| { | ||
| try { | ||
| $request->validate([ | ||
| 'desa_id' => 'required|integer:desa_id', |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax
Kategori: PHP Quality
Masalah: Syntax validasi salah - integer:desa_id bukan format valid Laravel. Seharusnya integer|exists:table,column
Kode: 'desa_id' => 'required|integer:desa_id',
Fix:
'desa_id' => 'required|integer|exists:das_data_desa,id',| 'nama' => 'required|string|max:255', | ||
| 'jumlah' => 'required|integer|min:0', | ||
| 'kategori' => 'required|string|max:100', | ||
| 'keterangan' => 'required|string:max:255', |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax
Kategori: PHP Quality
Masalah: Syntax validasi salah - string:max:255 bukan format valid. Seharusnya string|max:255
Kode: 'keterangan' => 'required|string:max:255',
Fix:
'keterangan' => 'nullable|string|max:255',| { | ||
| try { | ||
| $request->validate([ | ||
| 'desa_id' => 'required|integer:desa_id', |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax
Kategori: PHP Quality
Masalah: Syntax validasi salah - integer:desa_id bukan format valid Laravel. Seharusnya integer|exists:table,column
Kode: 'desa_id' => 'required|integer:desa_id',
Fix:
'desa_id' => 'required|integer|exists:das_data_desa,id',| 'nama' => 'required|string|max:255', | ||
| 'jumlah' => 'required|integer|min:0', | ||
| 'kategori' => 'required|string|max:100', | ||
| 'keterangan' => 'required|string:max:255', |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax
Kategori: PHP Quality
Masalah: Syntax validasi salah - string:max:255 bukan format valid. Seharusnya string|max:255
Kode: 'keterangan' => 'required|string:max:255',
Fix:
'keterangan' => 'nullable|string|max:255',| 'kategori' => 'required|string|max:100', | ||
| 'keterangan' => 'required|string:max:255', | ||
| ]); | ||
| DataSarana::create($request->all()); |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Mass Assignment Vulnerability
Kategori: Architecture
Masalah: Menggunakan $request->all() membuka celah mass assignment - user bisa inject field yang tidak diinginkan (misal: id, created_at, dll)
Kode: DataSarana::create($request->all());
Fix:
DataSarana::create($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// atau lebih baik:
DataSarana::create($request->validated());| 'keterangan' => 'required|string:max:255', | ||
| ]); | ||
| $sarana = DataSarana::findOrFail($id); | ||
| $sarana->update($request->all()); |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Mass Assignment Vulnerability
Kategori: Architecture
Masalah: Menggunakan $request->all() membuka celah mass assignment - user bisa inject field yang tidak diinginkan
Kode: $sarana->update($request->all());
Fix:
$sarana->update($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// atau lebih baik:
$sarana->update($request->validated());| return Excel::download(new ExportDataSarana($data, 'Admin Desa'), 'data_sarana.xlsx'); | ||
| } catch (\Exception $e) { | ||
| report($e); | ||
| return back()->withInput()->with('error', 'Data Sarana gagal dihapus'); |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Wrong Error Message
Kategori: PHP Quality
Masalah: Method export() menampilkan pesan error "gagal dihapus" yang tidak sesuai konteks - seharusnya "gagal diexport"
Kode: return back()->withInput()->with('error', 'Data Sarana gagal dihapus');
Fix:
return back()->with('error', 'Data Sarana gagal diexport');|
|
||
| $desas = DataDesa::all(); | ||
| return view('data.data_sarana.create', compact('page_title', 'page_description', 'desas')); | ||
| } |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing FormRequest Class
Kategori: Architecture
Masalah: Validasi dilakukan langsung di controller dengan $request->validate(). Best practice Laravel: buat FormRequest class untuk validasi terpusat dan reusable
Kode: Method store() dan update() melakukan validasi inline
Fix:
// 1. Buat app/Http/Requests/DataSaranaRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DataSaranaRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'desa_id' => 'required|integer|exists:das_data_desa,id',
'nama' => 'required|string|max:255',
'jumlah' => 'required|integer|min:0',
'kategori' => 'required|string|max:255',
'keterangan' => 'nullable|string|max:255',
];
}
}
// 2. Update controller methods:
public function store(DataSaranaRequest $request)
{
DataSarana::create($request->validated());
return redirect()->route('data.data-sarana.index')
->with('success', 'Data Sarana berhasil ditambahkan');
}
public function update(DataSaranaRequest $request, $id)
{
$sarana = DataSarana::findOrFail($id);
$sarana->update($request->validated());
return redirect()->route('data.data-sarana.index')
->with('success', 'Data Sarana berhasil diupdate');
}
🐛 Bug Detection ReviewTotal Temuan: 15 isu (5 Critical, 10 High)
|
| { | ||
| try { | ||
| $request->validate([ | ||
| 'desa_id' => 'required|integer:desa_id', |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax
Kode: 'desa_id' => 'required|integer:desa_id'
Skenario: Ketika user submit form create, Laravel validation akan throw exception karena rule 'integer:desa_id' adalah syntax yang salah. Rule 'integer' tidak menerima parameter dengan colon. Ini akan menyebabkan 500 error di production.
Dampak: Form create tidak bisa digunakan sama sekali. Setiap submit akan crash dengan error "Method Illuminate\Validation\Validator::validateInteger does not exist" atau validation gagal silent.
Fix:
'desa_id' => 'required|integer|exists:das_data_desa,id',| 'nama' => 'required|string|max:255', | ||
| 'jumlah' => 'required|integer|min:0', | ||
| 'kategori' => 'required|string|max:100', | ||
| 'keterangan' => 'required|string:max:255', |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax
Kode: 'keterangan' => 'required|string:max:255'
Skenario: Syntax validation salah, seharusnya 'string|max:255' bukan 'string:max:255'. Colon digunakan untuk parameter rule tertentu (seperti max:255), bukan untuk chain rule. Ini akan menyebabkan validation error atau exception.
Dampak: Form create akan crash atau validation tidak berjalan dengan benar. User tidak bisa menyimpan data sarana baru.
Fix:
'keterangan' => 'nullable|string|max:255',
// Ubah juga dari 'required' ke 'nullable' karena keterangan seharusnya optional| 'kategori' => 'required|string|max:100', | ||
| 'keterangan' => 'required|string:max:255', | ||
| ]); | ||
| DataSarana::create($request->all()); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Mass Assignment Vulnerability
Kode: DataSarana::create($request->all());
Skenario: Menggunakan $request->all() memungkinkan attacker mengirim field tambahan yang tidak diinginkan dalam POST request. Jika model tidak memiliki $fillable/$guarded yang ketat, field seperti 'id', 'created_at', atau field lain bisa di-inject.
Dampak: Security vulnerability. Attacker bisa manipulasi data dengan menambahkan field tidak authorized dalam request payload. Misalnya inject 'id' untuk overwrite record lain, atau field internal lainnya.
Fix:
DataSarana::create($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// Atau lebih baik:
DataSarana::create($request->validated());| $sarana = DataSarana::findOrFail($id); | ||
| $desas = DataDesa::all(); | ||
|
|
||
| return view('data.data_sarana.edit', compact('page_title', 'page_description', 'sarana', 'desas')); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Missing Null Check Before Method Call
Kode: $sarana = DataSarana::find($id); return view('data.data_sarana.edit', compact('sarana', 'desas'));
Skenario: Jika DataSarana::find($id) tidak menemukan record (return null), view akan menerima $sarana = null. Ketika blade template mencoba akses $sarana->nama, $sarana->kategori, dll, akan terjadi error "Attempt to read property on null".
Dampak: User yang akses URL /edit/999 (ID tidak exist) akan mendapat 500 error instead of 404. Ini juga bisa terjadi karena race condition (record dihapus setelah user buka halaman list tapi sebelum klik edit).
Fix:
$sarana = DataSarana::findOrFail($id);
// findOrFail() otomatis throw 404 jika record tidak ditemukan| { | ||
| try { | ||
| $request->validate([ | ||
| 'desa_id' => 'required|integer:desa_id', |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax
Kode: 'desa_id' => 'required|integer:desa_id'
Skenario: Bug yang sama dengan line 66, tapi di method update(). Validation rule syntax salah akan menyebabkan exception saat user submit form edit.
Dampak: Form edit tidak bisa digunakan. Setiap update akan crash dengan validation error atau exception.
Fix:
'desa_id' => 'required|integer|exists:das_data_desa,id',| 'nama' => 'required|string|max:255', | ||
| 'jumlah' => 'required|integer|min:0', | ||
| 'kategori' => 'required|string|max:100', | ||
| 'keterangan' => 'required|string:max:255', |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax
Kode: 'keterangan' => 'required|string:max:255'
Skenario: Bug yang sama dengan line 70, tapi di method update(). Syntax validation salah akan crash form edit.
Dampak: User tidak bisa update data sarana yang sudah ada. Form edit tidak fungsional.
Fix:
'keterangan' => 'nullable|string|max:255',| 'keterangan' => 'required|string:max:255', | ||
| ]); | ||
| $sarana = DataSarana::findOrFail($id); | ||
| $sarana->update($request->all()); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Mass Assignment Vulnerability
Kode: $sarana->update($request->all());
Skenario: Sama seperti line 72, menggunakan $request->all() di update memungkinkan injection field tidak diinginkan. Attacker bisa menambahkan field dalam PUT request untuk memodifikasi data yang seharusnya protected.
Dampak: Security vulnerability. Attacker bisa manipulasi field yang tidak seharusnya bisa diubah melalui form edit.
Fix:
$sarana->update($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// Atau:
$sarana->update($request->validated());| public function destroy($id) | ||
| { | ||
| try { | ||
| $sarana = DataSarana::findOrFail($id); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Missing Null Check Before Method Call
Kode: $sarana = DataSarana::find($id); $sarana->delete();
Skenario: Jika DataSarana::find($id) return null (record tidak ditemukan), memanggil ->delete() pada null akan throw fatal error "Call to a member function delete() on null". Ini bisa terjadi karena race condition (2 user delete record yang sama secara bersamaan) atau direct API call dengan ID invalid.
Dampak: Application crash dengan 500 error. User mendapat error page instead of graceful error message. Logs penuh dengan fatal error.
Fix:
$sarana = DataSarana::findOrFail($id);
$sarana->delete();
// Atau dengan manual check:
if (!$sarana) {
return back()->with('error', 'Data tidak ditemukan');
}| return Excel::download(new ExportDataSarana($data, 'Admin Desa'), 'data_sarana.xlsx'); | ||
| } catch (\Exception $e) { | ||
| report($e); | ||
| return back()->withInput()->with('error', 'Data Sarana gagal dihapus'); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Wrong Error Message
Kode: return back()->withInput()->with('error', 'Data Sarana gagal dihapus');
Skenario: Ini ada di dalam method export(), bukan destroy(). Ketika export gagal, user melihat pesan error "Data Sarana gagal dihapus" yang sangat misleading. User akan bingung kenapa ada pesan delete padahal mereka sedang export.
Dampak: User confusion. Troubleshooting jadi sulit karena error message tidak match dengan action yang dilakukan. User mungkin report bug yang salah.
Fix:
return back()->with('error', 'Data Sarana gagal diexport');
// Juga hapus withInput() karena tidak perlu di export| */ | ||
| public function model(array $row) | ||
| { | ||
| if (!DataDesa::where('id', $row['desa_id'])->exists()) { |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Silent Failure Without Logging
Kode: if (!DataDesa::where('id', $row['desa_id'])->exists()) { return null; }
Skenario: Ketika import Excel dengan 100 rows, jika 20 rows memiliki desa_id yang tidak valid, mereka akan di-skip secara silent (return null). User hanya melihat "Import berhasil" tapi tidak tahu bahwa 20 rows gagal. Tidak ada cara untuk track row mana yang gagal atau kenapa.
Dampak: Data loss tanpa notifikasi. User mengira semua data ter-import tapi sebenarnya ada yang hilang. Debugging sangat sulit karena tidak ada log. Production data bisa incomplete tanpa disadari.
Fix:
if (!DataDesa::where('id', $row['desa_id'])->exists()) {
\Log::warning('Import skipped: desa_id not found', [
'desa_id' => $row['desa_id'],
'row' => $row
]);
return null;
}| return null; | ||
| } | ||
|
|
||
| return new DataSarana([ |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Missing Array Key Validation
Kode: return new DataSarana([ 'desa_id' => $row['desa_id'], 'nama' => $row['nama'], ... ]);
Skenario: Jika user upload Excel file dengan kolom yang hilang (misalnya tidak ada kolom 'kategori'), akses $row['kategori'] akan throw "Undefined array key 'kategori'" error di PHP 8+. Import akan crash di tengah jalan.
Dampak: Import gagal total dengan 500 error. User tidak mendapat feedback yang jelas tentang apa yang salah dengan file Excel mereka. Harus trial-error untuk menemukan kolom mana yang hilang.
Fix:
// Tambahkan validasi di awal method model()
if (!isset($row['desa_id'], $row['nama'], $row['jumlah'], $row['kategori'])) {
\Log::warning('Import skipped: missing required columns', ['row' => $row]);
return null;
}
return new DataSarana([
'desa_id' => $row['desa_id'],
'nama' => $row['nama'],
'jumlah' => $row['jumlah'],
'kategori' => $row['kategori'],
'keterangan' => $row['keterangan'] ?? null,
]);| ajax: { | ||
| url: "{{ route('data.data-sarana.getdata') }}", | ||
| data: function (d) { | ||
| d.desa_id = $('#desa_id').val(); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Missing Error Handling for AJAX
Kode: ajax: { url: '{{ route('data.data-sarana.getdata') }}', data: function(d) { ... } }
Skenario: DataTables AJAX configuration tidak memiliki error handler. Jika server return 500 error (misalnya bug di getData() method), atau network timeout, atau server down, user hanya melihat generic DataTables error "Ajax error" tanpa informasi berguna.
Dampak: Poor user experience. User tidak tahu apa yang salah - apakah masalah network, server error, atau bug aplikasi. Admin tidak mendapat error report yang jelas untuk debugging.
Fix:
ajax: {
url: '{{ route('data.data-sarana.getdata') }}',
data: function(d) {
d.desa_id = $('#filter-desa').val();
d.kategori = $('#filter-kategori').val();
},
error: function(xhr, error, thrown) {
console.error('DataTables error:', error, thrown);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Gagal memuat data. Silakan refresh halaman atau hubungi administrator.'
});
}
}| table.ajax.reload(); | ||
| }); | ||
| }); | ||
| </script> |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Form Submit Without Error Handling
Kode: if (result.isConfirmed) { form.submit(); }
Skenario: Ketika user confirm delete via SweetAlert, form di-submit dengan form.submit() tanpa error handling. Jika server return 500 error, atau route tidak ditemukan (404), atau network error, user tidak mendapat feedback apapun. Page mungkin hang atau reload tanpa perubahan.
Dampak: User tidak tahu apakah delete berhasil atau gagal. Mereka harus manual refresh untuk cek. Jika gagal silent, user mengira data sudah terhapus padahal masih ada. Bisa menyebabkan data inconsistency.
Fix:
if (result.isConfirmed) {
$.ajax({
url: form.attr('action'),
method: 'POST',
data: form.serialize(),
success: function(response) {
Swal.fire('Berhasil', 'Data berhasil dihapus', 'success');
table.draw(); // Refresh DataTables
},
error: function(xhr) {
var message = xhr.responseJSON?.message || 'Gagal menghapus data';
Swal.fire('Error', message, 'error');
}
});
}
🤖 AI Code Review — Selesai📋 Ringkasan Semua Review
Total inline comments: 26 |

Menambahkan fitur Data Sarana untuk OpenDK:
CRUD (Create, Read, Update, Delete) Data Sarana.
Export Excel dengan header berwarna, title, nama, dan tanggal.
Rekap jumlah sarana per kategori.
Unit/feature test untuk semua route Data Sarana.
Integrasi filter pencarian, kategori, dan tanggal untuk export & index.
Penambahan pagination
Relation Table data sarana dan data desa
Masalah Terkait (Related Issue):
Menyelesaikan issue #939 terkait detail dan pengelolaan Data Sarana di OpenDK.
Langkah untuk Mereproduksi (Steps to Reproduce):
Buka halaman Data Sarana di OpenDK.
Tambahkan data baru melalui form “Tambah”.
Edit data melalui tombol “Edit”.
Hapus data melalui tombol “Hapus” dan konfirmasi.
Klik tombol Export untuk mendownload Excel.
Jalankan unit test:
php artisan test --filter=DataSaranaControllerTest
Pastikan semua test berhasil.
Daftar Periksa (Checklist):
Saya telah mengikuti aturan penulisan script OpenDK.
Saya telah membuat unit/feature test untuk semua route Data Sarana.
Semua fitur telah diuji di local development.
Form dan export Excel sudah sesuai spesifikasi.
Kode telah di-push ke branch 939-sarana-detail.
Tangkapan Layar (Screenshot):