diff --git a/app/Enums/Modul.php b/app/Enums/Modul.php
index 77d2e8d0..8fb1b454 100644
--- a/app/Enums/Modul.php
+++ b/app/Enums/Modul.php
@@ -177,6 +177,18 @@ final class Modul extends Enum
'url' => 'data-presisi/adat',
'permission' => 'datapresisi-adat',
],
+ [
+ 'icon' => 'far fa-fw fa-circle',
+ 'text' => 'Laporan Pengisian',
+ 'url' => 'data-presisi/laporan',
+ 'permission' => 'datapresisi-laporan',
+ ],
+ [
+ 'icon' => 'far fa-fw fa-circle',
+ 'text' => 'Laporan Pengisian Perdesa',
+ 'url' => 'data-presisi/laporan/perdesa',
+ 'permission' => 'datapresisi-laporan',
+ ],
],
],
[
diff --git a/app/Http/Controllers/DataPresisiLaporanController.php b/app/Http/Controllers/DataPresisiLaporanController.php
new file mode 100644
index 00000000..33cd463d
--- /dev/null
+++ b/app/Http/Controllers/DataPresisiLaporanController.php
@@ -0,0 +1,40 @@
+ $request->getQueryString(), 'title' => $title]);
+ }
+
+ public function cetakPerdesa(Request $request)
+ {
+ $title = 'Data Presisi Pengisian Laporan Per Desa';
+ $namaDesa = session('desa.nama_desa') ?? 'Semua Desa';
+ return view('data_pokok.data_presisi.laporan.cetak_perdesa', [
+ 'filter' => $request->getQueryString(),
+ 'title' => $title,
+ 'namaDesa' => $namaDesa
+ ]);
+ }
+}
diff --git a/catatan_rilis.md b/catatan_rilis.md
index 135cde62..3f69d0e8 100644
--- a/catatan_rilis.md
+++ b/catatan_rilis.md
@@ -2,14 +2,10 @@ Di rilis ini, versi 2511.0.0 berisi penambahan dan perbaikan yang diminta penggu
#### Penambahan Fitur
-1. [#827](https://github.com/OpenSID/OpenKab/issues/827) Penambahan Fitur Login OTP Passwordless dengan Pengaturan Awal via Email/Telegram sebagai Alternatif Autentikasi di OpenKab.
-2. [#829](https://github.com/OpenSID/OpenKab/issues/829) Penambahan 2FA keamaan login.
+1. [#842](https://github.com/OpenSID/OpenKab/issues/842) Tambahkan fitur halaman baru laporan pengisian pada menu data presisi.
#### Perbaikan BUG
-1. [#836](https://github.com/OpenSID/OpenKab/issues/836) Perbaikan unit test.
#### Perubahan Teknis
-1. [#790](https://github.com/OpenSID/OpenKab/issues/790) Upgrade versi laravel dari 9.52.16 menjadi 10.48.29.
-2. [#791](https://github.com/OpenSID/OpenKab/issues/791) Upgrade teknis berdasarkan composer audit pada OpenKab.
diff --git a/resources/views/data_pokok/data_presisi/laporan/cetak.blade.php b/resources/views/data_pokok/data_presisi/laporan/cetak.blade.php
new file mode 100644
index 00000000..a95ddeaf
--- /dev/null
+++ b/resources/views/data_pokok/data_presisi/laporan/cetak.blade.php
@@ -0,0 +1,81 @@
+@extends('layouts.cetak.index')
+
+@section('title', $title)
+
+@push('css')
+
+@endpush
+
+@section('content')
+ @include('partials.breadcrumbs')
+
+
+
+ | No |
+ Desa |
+ Pangan |
+ Sandang |
+ Papan |
+ Pendidikan |
+ Seni Budaya |
+ Kesehatan |
+ Keagamaan |
+ Jaminan Sosial |
+ Adat |
+ Ketenagakerjaan |
+ Jumlah Penduduk |
+
+
+
+
+@stop
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/data_pokok/data_presisi/laporan/cetak_perdesa.blade.php b/resources/views/data_pokok/data_presisi/laporan/cetak_perdesa.blade.php
new file mode 100644
index 00000000..db0004ce
--- /dev/null
+++ b/resources/views/data_pokok/data_presisi/laporan/cetak_perdesa.blade.php
@@ -0,0 +1,71 @@
+@extends('layouts.cetak.index')
+
+@section('title', $title)
+
+@push('css')
+
+@endpush
+
+@section('content')
+ @include('partials.breadcrumbs')
+ {{-- @dd($filter) --}}
+
+
Desa: {{ $namaDesa }}
+
+
+
+
+ | No |
+ Uraian |
+ Data Lengkap |
+ Lengkap Sebagian |
+ Tidak Lengkap |
+ Total Data |
+
+
+
+
+@stop
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/data_pokok/data_presisi/laporan/chart.blade.php b/resources/views/data_pokok/data_presisi/laporan/chart.blade.php
new file mode 100644
index 00000000..83cf2b84
--- /dev/null
+++ b/resources/views/data_pokok/data_presisi/laporan/chart.blade.php
@@ -0,0 +1,91 @@
+
+@push('css')
+
+@endpush
\ No newline at end of file
diff --git a/resources/views/data_pokok/data_presisi/laporan/index.blade.php b/resources/views/data_pokok/data_presisi/laporan/index.blade.php
new file mode 100644
index 00000000..ff806ca5
--- /dev/null
+++ b/resources/views/data_pokok/data_presisi/laporan/index.blade.php
@@ -0,0 +1,252 @@
+@extends('layouts.index')
+
+@section('title', $title)
+
+@section('content_header')
+ {{ $title }}
+@stop
+
+@push('css')
+
+@endpush
+
+@section('content')
+ @include('partials.breadcrumbs')
+
+
+
+
+
+
+
+
+
+ | No |
+ Desa |
+ Pangan |
+ Sandang |
+ Papan |
+ Pendidikan |
+ Seni Budaya |
+ Kesehatan |
+ Keagamaan |
+ Jaminan Sosial |
+ Adat |
+ Ketenagakerjaan |
+ Jumlah Penduduk |
+ Jumlah Rumah Tangga |
+
+
+
+
+
+
+
+
+
+@endsection
+
+@section('js')
+
+
+@endsection
diff --git a/resources/views/data_pokok/data_presisi/laporan/perdesa.blade.php b/resources/views/data_pokok/data_presisi/laporan/perdesa.blade.php
new file mode 100644
index 00000000..fe80b669
--- /dev/null
+++ b/resources/views/data_pokok/data_presisi/laporan/perdesa.blade.php
@@ -0,0 +1,203 @@
+@extends('layouts.index')
+
+@section('title', $title)
+
+@section('content_header')
+ {{ $title }}
+@stop
+
+@push('css')
+
+@endpush
+
+@section('content')
+ @include('partials.breadcrumbs')
+
+
+
+
+
+
+
+
+
+ | No |
+ Uraian |
+ Data Lengkap |
+ Lengkap Sebagian |
+ Tidak Lengkap |
+ Total Data |
+
+
+
+
+
+
+
+
+
+@endsection
+
+@section('js')
+
+
+@endsection
diff --git a/resources/views/data_pokok/data_presisi/pangan/index.blade.php b/resources/views/data_pokok/data_presisi/pangan/index.blade.php
index fe6c14fc..5de4cb3a 100644
--- a/resources/views/data_pokok/data_presisi/pangan/index.blade.php
+++ b/resources/views/data_pokok/data_presisi/pangan/index.blade.php
@@ -34,8 +34,9 @@
$currentYear = date('Y');
$startYear = 2020;
@endphp
- @for($year = $currentYear; $year >= $startYear; $year--)
-
+ @for ($year = $currentYear; $year >= $startYear; $year--)
+
@endfor
@@ -75,7 +76,7 @@
let data_grafik = [];
document.addEventListener("DOMContentLoaded", function(event) {
const header = @include('layouts.components.header_bearer_api_gabungan');
- var url = new URL("{{ config('app.databaseGabunganUrl').'/api/v1/data-presisi/pangan/rtm' }}");
+ var url = new URL("{{ config('app.databaseGabunganUrl') . '/api/v1/data-presisi/pangan/rtm' }}");
url.searchParams.set("kode_kabupaten", "{{ session('kabupaten.kode_kabupaten') ?? '' }}");
url.searchParams.set("kode_kecamatan", "{{ session('kecamatan.kode_kecamatan') ?? '' }}");
url.searchParams.set("kode_desa", "{{ session('desa.id') ?? '' }}");
@@ -90,7 +91,7 @@
},
ajax: {
url: url.href,
- headers: header,
+ headers: header,
method: 'get',
data: function(row) {
return {
@@ -104,29 +105,27 @@
},
dataSrc: function(json) {
if (json.data.length > 0) {
- json.recordsTotal = json.meta.pagination.total
- json.recordsFiltered = json.meta.pagination.total
- data_grafik = [];
- json.data.forEach(function(item, index) {
- data_grafik.push(item.attributes)
- })
- grafikPie()
- return json.data;
- }
- return false;
+ json.recordsTotal = json.meta.pagination.total
+ json.recordsFiltered = json.meta.pagination.total
+ data_grafik = [];
+ json.data.forEach(function(item, index) {
+ data_grafik.push(item.attributes)
+ })
+ grafikPie()
+ return json.data;
+ }
+ return false;
},
},
columnDefs: [{
- targets: '_all',
- className: 'text-nowrap',
- },
- ],
- columns: [
- {
+ targets: '_all',
+ className: 'text-nowrap',
+ }, ],
+ columns: [{
data: function(data) {
let d = data.attributes
let obj = {
- 'rtm_id' : data.id,
+ 'rtm_id': data.id,
'no_kartu_rumah': d.no_kk,
'nama_kepala_keluarga': d.kepala_keluarga,
'alamat': d.alamat,
@@ -134,7 +133,9 @@ className: 'text-nowrap',
'jumlah_kk': d.jumlah_kk,
}
let jsonData = encodeURIComponent(JSON.stringify(obj));
- const _url = "{{ route('data-pokok.data-presisi-pangan.detail', ['data' => '__DATA__']) }}".replace('__DATA__', jsonData)
+ const _url =
+ "{{ route('data-pokok.data-presisi-pangan.detail', ['data' => '__DATA__']) }}"
+ .replace('__DATA__', jsonData)
return `
`;
@@ -166,11 +167,11 @@ className: 'text-nowrap',
data: "attributes.luas_lahan",
render: (data) => data || 'N/A',
},
-
+
],
})
// Add event listener for opening and closing details
- dtks.on('click', 'td.details-control', function () {
+ dtks.on('click', 'td.details-control', function() {
let tr = $(this).closest('tr');
let row = dtks.row(tr);
if (row.child.isShown()) {
@@ -183,6 +184,7 @@ className: 'text-nowrap',
tr.addClass('shown');
}
});
+
function format(data) {
return `
@@ -267,7 +269,7 @@ function format(data) {
data_grafik = [];
grafikPie();
});
-
+
$('#cetak').on('click', function() {
let baseUrl = "{{ route('data-pokok.data-presisi-pangan.cetak') }}";
let params = dtks.ajax.params(); // Get DataTables params
@@ -276,4 +278,4 @@ function format(data) {
});
})
-@endsection
\ No newline at end of file
+@endsection
diff --git a/routes/web.php b/routes/web.php
index 62c4cc6e..a7954bc0 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -270,6 +270,14 @@
});
Route::prefix('data-presisi')->group(function () {
+ Route::prefix('laporan')->group(function () {
+ Route::get('/', [App\Http\Controllers\DataPresisiLaporanController::class, 'index'])->name('laporan.data-presisi.index');
+ Route::get('cetak', [App\Http\Controllers\DataPresisiLaporanController::class, 'cetak'])->name('laporan.data-presisi.cetak');
+ Route::get('/perdesa', [App\Http\Controllers\DataPresisiLaporanController::class, 'perdesa'])->name('laporan.data-presisi.perdesa');
+ Route::get('/cetak-perdesa', [App\Http\Controllers\DataPresisiLaporanController::class, 'cetakPerdesa'])->name('laporan.data-presisi.cetak-perdesa');
+ })
+ ->middleware(['permission:datapresisi-laporan-read']);
+
Route::prefix('kesehatan')->group(function () {
Route::get('/', [App\Http\Controllers\DataPresisiKesehatanController::class, 'index'])->name('data-pokok.data-presisi.index');
Route::get('/detail', [App\Http\Controllers\DataPresisiKesehatanController::class, 'detail'])->name('data-pokok.data-presisi.detail');
diff --git a/tests/Feature/DataPresisiLaporanTest.php b/tests/Feature/DataPresisiLaporanTest.php
new file mode 100644
index 00000000..ca59b343
--- /dev/null
+++ b/tests/Feature/DataPresisiLaporanTest.php
@@ -0,0 +1,313 @@
+get(route('laporan.data-presisi.index'));
+
+ $response->assertStatus(200);
+ $response->assertViewIs('data_pokok.data_presisi.laporan.index');
+ $response->assertViewHas('title', 'Data Presisi Laporan Semua Desa');
+ }
+
+ /** @test */
+ public function test_laporan_semua_desa_has_required_elements()
+ {
+ $response = $this->get(route('laporan.data-presisi.index'));
+
+ $content = $response->getContent();
+
+ // Test DataTable exists
+ $this->assertStringContainsString('id="laporanTable"', $content);
+
+ // Test filter status exists
+ $this->assertStringContainsString('id="filter-status"', $content);
+
+ // Test status options exist
+ $this->assertStringContainsString('Tidak Lengkap', $content);
+ $this->assertStringContainsString('Lengkap Sebagian', $content);
+ $this->assertStringContainsString('Data Lengkap', $content);
+ }
+
+ /** @test */
+ public function test_laporan_semua_desa_has_correct_table_columns()
+ {
+ $response = $this->get(route('laporan.data-presisi.index'));
+
+ $content = $response->getContent();
+
+ // Test table headers exist
+ $expectedColumns = [
+ 'Desa',
+ 'Pangan',
+ 'Sandang',
+ 'Papan',
+ 'Pendidikan',
+ 'Seni Budaya',
+ 'Kesehatan',
+ 'Keagamaan',
+ 'Jaminan Sosial',
+ 'Adat',
+ 'Ketenagakerjaan',
+ 'Jumlah Penduduk'
+ ];
+
+ foreach ($expectedColumns as $column) {
+ $this->assertStringContainsString($column, $content);
+ }
+ }
+
+ /** @test */
+ public function test_laporan_semua_desa_has_filter_status_javascript()
+ {
+ $response = $this->get(route('laporan.data-presisi.index'));
+
+ $content = $response->getContent();
+
+ // Test filter status change event listener exists
+ $this->assertStringContainsString("$('#filter-status').on('change'", $content);
+ $this->assertStringContainsString("$('#laporanTable').DataTable().ajax.reload()", $content);
+ }
+
+ /** @test */
+ public function test_laporan_semua_desa_has_datatable_configuration()
+ {
+ $response = $this->get(route('laporan.data-presisi.index'));
+
+ $content = $response->getContent();
+
+ // Test DataTable configuration
+ $this->assertStringContainsString('processing: true', $content);
+ $this->assertStringContainsString('serverSide: true', $content);
+
+ // Test filter[status_kelengkapan] parameter
+ $this->assertStringContainsString('"filter[status_kelengkapan]"', $content);
+ $this->assertStringContainsString("$('#filter-status').val()", $content);
+ }
+
+ /** @test */
+ public function test_can_access_laporan_perdesa_page()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $response->assertStatus(200);
+ $response->assertViewIs('data_pokok.data_presisi.laporan.perdesa');
+ $response->assertViewHas('title', 'Data Presisi Laporan Per Desa');
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_required_filter_elements()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test all filter elements exist
+ $this->assertStringContainsString('id="filter_kabupaten"', $content);
+ $this->assertStringContainsString('id="filter_kecamatan"', $content);
+ $this->assertStringContainsString('id="filter_desa"', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_correct_table_columns()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test table headers exist
+ $expectedColumns = [
+ 'Uraian',
+ 'Data Lengkap',
+ 'Lengkap Sebagian',
+ 'Tidak Lengkap',
+ 'Total Data'
+ ];
+
+ foreach ($expectedColumns as $column) {
+ $this->assertStringContainsString($column, $content);
+ }
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_select2_initialization()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test Select2 initialization for all filters
+ $this->assertStringContainsString("$('#filter_kabupaten').select2(", $content);
+ $this->assertStringContainsString("$('#filter_kecamatan').select2(", $content);
+ $this->assertStringContainsString("$('#filter_desa').select2(", $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_cascading_filter_logic()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test kabupaten change event
+ $this->assertStringContainsString("$('#filter_kabupaten').on('change'", $content);
+
+ // Test kecamatan change event
+ $this->assertStringContainsString("$('#filter_kecamatan').on('change'", $content);
+
+ // Test desa change event with reload
+ $this->assertStringContainsString("$('#filter_desa').on('change'", $content);
+ $this->assertStringContainsString('laporanTable.ajax.reload()', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_loads_kabupaten_from_api()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test API call to get kabupaten list
+ $this->assertStringContainsString('/api/v1/statistik-web/get-list-kabupaten', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_loads_kecamatan_from_api()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test API call to get kecamatan list
+ $this->assertStringContainsString('/api/v1/statistik-web/get-list-kecamatan', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_loads_desa_from_api()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test API call to get desa list
+ $this->assertStringContainsString('/api/v1/statistik-web/get-list-desa', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_datatable_with_filters()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test DataTable configuration
+ $this->assertStringContainsString('processing: true', $content);
+ $this->assertStringContainsString('serverSide: true', $content);
+ $this->assertStringContainsString('searching: false', $content);
+
+ // Test filter parameters in DataTable
+ $this->assertStringContainsString('"filter[config_id]"', $content);
+ $this->assertStringContainsString('"kode_kabupaten"', $content);
+ $this->assertStringContainsString('"kode_kecamatan"', $content);
+ $this->assertStringContainsString('"config_desa"', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_default_session_values()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test that session default values are being set (even if empty)
+ // Check that variables exist in JavaScript code
+ $this->assertStringContainsString('defaultKabupaten', $content);
+ $this->assertStringContainsString('defaultKecamatan', $content);
+ $this->assertStringContainsString('defaultDesa', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_has_prevent_reload_flag()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test preventReload flag to avoid cascading reloads
+ $this->assertStringContainsString('preventReload', $content);
+ $this->assertStringContainsString('preventReload = true', $content);
+ $this->assertStringContainsString('preventReload = false', $content);
+ $this->assertStringContainsString('if (!preventReload', $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_disables_child_filters_initially()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test that child filters are disabled until parent is selected
+ $this->assertStringContainsString("$('#filter_kecamatan').prop('disabled', true)", $content);
+ $this->assertStringContainsString("$('#filter_desa').prop('disabled', true)", $content);
+ }
+
+ /** @test */
+ public function test_laporan_perdesa_number_formatting_for_columns()
+ {
+ $response = $this->get(route('laporan.data-presisi.perdesa'));
+
+ $content = $response->getContent();
+
+ // Test number formatting render for numeric columns
+ $this->assertStringContainsString("render: $.fn.dataTable.render.number('.', ',', 0, '')", $content);
+ }
+
+ /** @test */
+ public function test_both_pages_use_correct_api_endpoints()
+ {
+ // Test laporan index
+ $response1 = $this->get(route('laporan.data-presisi.index'));
+ $content1 = $response1->getContent();
+ $this->assertStringContainsString('/api/v1/data-presisi/laporan', $content1);
+
+ // Test laporan perdesa
+ $response2 = $this->get(route('laporan.data-presisi.perdesa'));
+ $content2 = $response2->getContent();
+ $this->assertStringContainsString('/api/v1/data-presisi/laporan-perdesa', $content2);
+ }
+
+ /** @test */
+ public function test_both_pages_have_breadcrumbs()
+ {
+ // Test laporan index has breadcrumb section
+ $response1 = $this->get(route('laporan.data-presisi.index'));
+ $response1->assertStatus(200);
+
+ // Test laporan perdesa has breadcrumb section
+ $response2 = $this->get(route('laporan.data-presisi.perdesa'));
+ $response2->assertStatus(200);
+
+ $this->assertTrue(true);
+ }
+
+ /** @test */
+ public function test_both_pages_extend_correct_layout()
+ {
+ // Test laporan index uses correct layout (has main-footer)
+ $response1 = $this->get(route('laporan.data-presisi.index'));
+ $content1 = $response1->getContent();
+ $this->assertStringContainsString('class="main-footer"', $content1);
+
+ // Test laporan perdesa uses correct layout (has main-footer)
+ $response2 = $this->get(route('laporan.data-presisi.perdesa'));
+ $content2 = $response2->getContent();
+ $this->assertStringContainsString('class="main-footer"', $content2);
+ }
+}