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') + + + + + + + + + + + + + + + + + + + +
NoDesaPanganSandangPapanPendidikanSeni BudayaKesehatanKeagamaanJaminan SosialAdatKetenagakerjaanJumlah 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 }}

+
+ + + + + + + + + + + + +
NoUraianData LengkapLengkap SebagianTidak LengkapTotal 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') +
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
NoDesaPanganSandangPapanPendidikanSeni BudayaKesehatanKeagamaanJaminan SosialAdatKetenagakerjaanJumlah PendudukJumlah 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') +
+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + +
NoUraianData LengkapLengkap SebagianTidak LengkapTotal 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); + } +}