From 1f11022c8bb92e27f1009133038de23270225090 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 6 May 2026 09:03:33 +0700 Subject: [PATCH 01/19] Fix/detail statistik pangan tidak tampil (#989) * fix: data detail statistik tidak tampil * fix: perbaikan detail presisi statistik pangan * fix: perbaikan detail presisi statistik pangan * Tambahkan test * hapus * fix: tambahkan link detail untuk belum mengisi, jumlah dan total agar seragam * perbaikan judul * perbaikan filter tahun * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- .gitignore | 2 +- QWEN.md | 334 ++++++++++++++++++ .../DataPresisiPanganController.php | 9 +- catatan_rilis.md | 21 +- .../data_presisi/pangan/detail_data.blade.php | 2 +- .../views/presisi/statistik/pangan.blade.php | 43 ++- .../DataPresisiPanganControllerTest.php | 121 +++++++ 7 files changed, 485 insertions(+), 47 deletions(-) create mode 100644 QWEN.md create mode 100644 tests/Feature/DataPresisiPanganControllerTest.php diff --git a/.gitignore b/.gitignore index 3ada7ae9..5615236f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ package-lock.json /test-results/ /playwright-report/ .env.e2e -/template_ai/ \ No newline at end of file +/template_ai/ diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 00000000..5a507b94 --- /dev/null +++ b/QWEN.md @@ -0,0 +1,334 @@ +# OpenKab - Project Context + +## Project Overview + +**OpenKab** is a Laravel-based web application designed for county governments (Kabupaten) across Indonesia. It integrates with OpenSID to display public statistics including population, health, education, and other demographic data. The application promotes transparency and public information disclosure. + +- **Repository**: https://github.com/OpenSID/OpenKab +- **Demo**: https://devopenkab.opendesa.id +- **Framework**: Laravel 10.x +- **PHP Version**: ^8.1 +- **Architecture**: MVC (Model-View-Controller) with Laravel + +## Tech Stack + +### Backend +- **Framework**: Laravel 10.48 +- **Language**: PHP 8.1+ +- **Database**: MySQL (dual database setup - main app + OpenSID combined data) +- **Authentication**: Laravel Sanctum +- **Key Packages**: + - `spatie/laravel-permission` - Role & permission management + - `spatie/laravel-activitylog` - Activity logging + - `yajra/laravel-datatables` - DataTables integration + - `bensampo/laravel-enum` - Enum support + - `intervention/image` - Image manipulation + - `jeroennoten/laravel-adminlte` - AdminLTE integration + - `kalnoy/nestedset` - Nested set model for hierarchies + +### Frontend +- **Build Tool**: Vite 4.x +- **CSS Framework**: Bootstrap 4.6.2, AdminLTE 3.2.0 +- **JavaScript**: jQuery, Alpine.js +- **UI Components**: + - Select2, Bootstrap Datepicker, Bootstrap Colorpicker + - SweetAlert2, TinyMCE, OWL Carousel + - FontAwesome 6, Bootstrap Icons + +### Testing +- **Unit/Feature Tests**: PHPUnit 10.x +- **E2E Tests**: Playwright (TypeScript) +- **Linting**: Laravel Pint, PHP CS Fixer + +## Project Structure + +``` +OpenKab/ +├── app/ # Application logic +│ ├── Console/ # Artisan commands +│ ├── Enums/ # PHP enums +│ ├── Http/ # Controllers, Middleware, Requests +│ ├── Models/ # Eloquent models +│ ├── Policies/ # Authorization policies +│ ├── Services/ # Business logic services +│ └── View/ # View composers +├── bootstrap/ # Application bootstrap files +├── config/ # Configuration files +├── database/ +│ ├── factories/ # Model factories for testing +│ ├── migrations/ # Database migrations +│ ├── seeders/ # Database seeders +│ └── maxmind/ # GeoIP database +├── docs/ # Documentation (currently empty) +├── lang/ # Localization files +├── public/ # Public assets (entry point) +├── resources/ +│ ├── js/ # JavaScript source files +│ ├── sass/ # SCSS source files +│ └── views/ # Blade templates +├── routes/ +│ ├── web.php # Web routes +│ ├── api.php # API routes +│ ├── apiv1.php # API v1 routes +│ ├── console.php # Console routes +│ └── breadcrumbs.php # Breadcrumb definitions +├── storage/ # Logs, cache, uploads +├── tests/ +│ ├── Unit/ # Unit tests +│ ├── Feature/ # Feature tests +│ ├── e2e/ # Playwright E2E tests +│ └── global-setup.js # E2E test global setup +└── artisan # Laravel CLI +``` + +## Building and Running + +### Prerequisites +- PHP 8.1+ with required extensions +- Composer +- Node.js 18+ +- MySQL 5.7+ or MariaDB +- Redis (optional) + +### Installation + +```bash +# 1. Install PHP dependencies +composer install + +# 2. Install Node.js dependencies +npm install + +# 3. Copy environment file +cp .env.example .env + +# 4. Generate application key +php artisan key:generate + +# 5. Configure database in .env file +# Edit DB_DATABASE, DB_USERNAME, DB_PASSWORD + +# 6. Run migrations +php artisan migrate + +# 7. Seed database (optional) +php artisan db:seed + +# 8. Build frontend assets +npm run build + +# 9. Start development server +php artisan serve +``` + +### Development Commands + +```bash +# Start Vite dev server (hot module replacement) +npm run dev + +# Build production assets +npm run build + +# Build for web (with config replacement) +npm run build-web + +# Run PHPUnit tests +php artisan test + +# Run E2E tests +npm run test:e2e + +# Run E2E tests with UI +npm run test:e2e:ui + +# Run E2E tests in headed mode +npm run test:e2e:headed + +# Show E2E test report +npm run test:e2e:report + +# Code style fixer +./vendor/bin/php-cs-fixer fix + +# Laravel Pint (alternative linter) +./vendor/bin/pint +``` + +### Artisan Commands + +```bash +# Standard Laravel commands available: +php artisan migrate # Run migrations +php artisan migrate:fresh # Fresh migrate +php artisan db:seed # Seed database +php artisan make:model # Generate model +php artisan make:controller # Generate controller +php artisan make:request # Generate form request +php artisan route:list # List all routes +php artisan config:cache # Cache configuration +php artisan view:clear # Clear compiled views +``` + +## Configuration + +### Environment Variables (from .env.example) + +Key configuration options: + +```ini +# Application +APP_NAME=OpenKab +APP_ENV=development +APP_DEBUG=false +APP_URL=http://devopenkab.opendesa.id/ + +# Main Database +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=testing_db +DB_USERNAME=root +DB_PASSWORD=secret + +# OpenSID Combined Database (secondary) +OPENKAB_DB_HOST=127.0.0.1 +OPENKAB_DB_PORT=3 Playwright configuration +OPENKAB_DB_DATABASE=testing_gabungan_db +OPENKAB_DB_USERNAME=root +OPENKAB_DB_PASSWORD=secret + +# Map Configuration +LATTITUDE_MAP=-8.459556 +LONGITUDE_MAP=115.046600 + +# CSP (Content Security Policy) +CSP_ENABLED=true + +# FTP Configuration (for data sync) +FTP_1_HOST= +FTP_1_URL= +FTP_1_USERNAME= +FTP_1_PASSWORD= +``` + +### Dual Database Setup + +The application uses two database connections: +1. **Default (mysql)**: Main application data +2. **opensid (OPENKAB_*)**: Combined OpenSID data for statistics + +## Testing + +### Unit/Feature Tests +- Located in `tests/Unit` and `tests/Feature` +- Run with `php artisan test` or `./vendor/bin/phpunit` +- Uses in-memory SQLite or test MySQL database + +### E2E Tests (Playwright) +- Located in `tests/e2e` +- Configuration in `playwright.config.js` +- Requires running application server +- Uses authentication state from `test-results/storage-state/auth.json` +- Global setup in `tests/global-setup.js` + +### Test Credentials (Demo) +- **Email**: admin@gmail.com +- **Password**: Admin100% + +## Development Conventions + +### Code Style +- **PHP CS Fixer**: Configured in `.php-cs-fixer.php` + - PSR-12 based with Laravel conventions + - Short array syntax + - Single quotes preferred + - Blank lines between class methods/properties +- **Laravel Pint**: Alternative PHP linter + +### File Organization +- Models in `app/Models/` +- Controllers in `app/Http/Controllers/` +- Form Requests in `app/Http/Requests/` +- Services in `app/Services/` +- Policies in `app/Policies/` + +### Naming Conventions +- Models: PascalCase (e.g., `User`, `Penduduk`) +- Controllers: PascalCase with `Controller` suffix +- Migrations: `YYYY_MM_DD_HHMMSS_create_table_name.php` +- Routes: kebab-case for URL segments + +### Database +- Migrations stored in `database/migrations/` +- Seeders in `database/seeders/` +- Factories in `database/factories/` +- Uses `eloquent-sluggable` for URL-friendly slugs +- Uses `nestedset` for hierarchical data + +### Validation +- **All form validation MUST use Form Request classes** (located in `app/Http/Requests/`) +- Never validate directly in controllers using `$request->validate()` +- Create dedicated Form Request classes using `php artisan make:request Request` +- Form requests centralize validation rules and authorization logic + +### Testing +- **DO NOT use `RefreshDatabase` trait** - it truncates tables and can cause issues with foreign key constraints +- **Use `DatabaseTransactions` trait instead** - wraps tests in database transactions +- Follow existing test patterns in `tests/Feature/` and `tests/Unit/` +- Use model factories for creating test data +- Test both success and failure scenarios +- **CORS Security Tests**: Located in `tests/Feature/CorsSecurityTest.php` + - Tests verify CORS configuration is secure + - Validates allowed origins are restricted (no wildcard with credentials) + - Tests preflight requests from allowed/non-allowed origins + - Run with: `php artisan test --filter CorsSecurityTest` + +### Security +- Content Security Policy (CSP) enabled via `spatie/laravel-csp` +- Laravel Sanctum for API authentication +- Spatie Permission for role-based access control +- Activity logging via `spatie/laravel-activitylog` +- **CORS (Cross-Origin Resource Sharing)**: + - Configured in `config/cors.php` + - `allowed_origins` restricted to trusted domains only (via `CORS_ALLOWED_ORIGINS` env variable) + - Never use wildcard (`*`) with `supports_credentials=true` + - Default allowed origins: production domain + localhost for development + - `allowed_headers` limited to: `Content-Type`, `Authorization`, `X-Requested-With`, `X-XSRF-TOKEN` + +## API + +### API Versions +- **v1**: Routes defined in `routes/apiv1.php` +- **Current**: Routes defined in `routes/api.php` + +### Authentication +- Sanctum token-based authentication +- Stateful domains configured via `SANCTUM_STATEFUL_DOMAINS` + +## Key Features + +1. **Statistics Dashboard**: Population, health, education statistics +2. **Data Integration**: Sync with OpenSID databases +3. **User Management**: Role-based permissions +4. **Activity Logging**: Track user actions +5. **File Manager**: Integrated file management +6. **Breadcrumbs**: Navigation breadcrumbs +7. **Visitor Tracking**: Track page visits +8. **Location Services**: GeoIP-based location detection +9. **OTP System**: Two-factor authentication support +10. **Telegram Bot**: Notification integration + +## Known Issues / Technical Notes + +- N+1 query problems addressed in user management (Issue #943) +- Year filter added for statistics boards (Issues #946, #948) +- FTP integration for remote data synchronization +- Rate limiting configurable for OTP and general API + +## Useful Links + +- **Demo**: https://devopenkab.opendesa.id +- **GitHub**: https://github.com/OpenSID/OpenKab +- **Laravel Docs**: https://laravel.com/docs +- **AdminLTE**: https://adminlte.io/ diff --git a/app/Http/Controllers/DataPresisiPanganController.php b/app/Http/Controllers/DataPresisiPanganController.php index 0f4f10b7..34abf54a 100644 --- a/app/Http/Controllers/DataPresisiPanganController.php +++ b/app/Http/Controllers/DataPresisiPanganController.php @@ -16,10 +16,13 @@ public function index() public function detailData() { $colomn = ''; - $title = 'Data Presisi Pangan '.request('judul'); + $title = 'Data Presisi Pangan - ' . request('judul'); $filter = request('filter'); - if($filter['tipe'] && $filter['nilai']){ - $colomn = $filter['tipe'].':'.$filter['nilai']; + + if (isset($filter['tipe']) && isset($filter['nilai'])) { + if ($filter['tipe'] && $filter['nilai']) { + $colomn = $filter['tipe'] . ':' . $filter['nilai']; + } } return view('data_pokok.data_presisi.pangan.detail_data', compact('title', 'colomn')); } diff --git a/catatan_rilis.md b/catatan_rilis.md index 1a4fbd50..30f46622 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -2,32 +2,15 @@ Di rilis ini, versi 2604.0.1 berisi penambahan dan perbaikan yang diminta penggu #### Penambahan Fitur -1. [#946](https://github.com/OpenSID/OpenKab/issues/946) Penambahan filter tahun pada statistik papan & sandang data presisi. -2. [#948](https://github.com/OpenSID/OpenKab/issues/948) Penambahan filter tahun pada statistik seni budaya & pendidikan data presisi. -3. [#952](https://github.com/OpenSID/OpenKab/issues/952) Penambahan filter tahun pada statistik Aktivitas Keagamaan, ketenagakerjaan dan adat data presisi. -4. [#942](https://github.com/OpenSID/OpenKab/issues/942) Penambahan fitur menampilkan artikel OpenSID di halaman publik. -5. [#372](https://github.com/OpenSID/API-Database-Gabungan/issues/372) Penambahan judul dan kategori ketika hapus artikel. -6. [#988](https://github.com/OpenSID/OpenKab/issues/988) Sesuaikan sort di datapresisi pangan. -7. [#995](https://github.com/OpenSID/OpenKab/issues/995) Penambahan fitur untuk laporan keaktifan desa melalui beberapa acuan. -1. [#372](https://github.com/OpenSID/API-Database-Gabungan/issues/372) Penambahan judul dan kategori ketika hapus artikel. +1. [#997](https://github.com/OpenSID/OpenKab/issues/997) Buat Halaman Detail Pangan di Statistik Presisi + #### Perbaikan BUG -1. [#954](https://github.com/OpenSID/OpenKab/issues/954) Perbaikan list menu tidak tampil. -2. [#369](https://github.com/OpenSID/API-Database-Gabungan/issues/369) Perbaikan cache artikel tidak dihapus setelah operasi hapus. -3. [#1015](https://github.com/OpenSID/OpenKab/issues/1015) Batasi opsi kabupaten berdasarkan pengaturan yang ada di API satu data. #### Perubahan Teknis -1. [#943](https://github.com/OpenSID/OpenKab/issues/943) N+1 Query problem pada manajemen user. -2. [#969](https://github.com/OpenSID/OpenKab/issues/969) Terapkan CAPTCHA pada Login & Endpoint Auth untuk Batasi Bot/Bruteforce. -3. [#962](https://github.com/OpenSID/OpenKab/issues/962) Pencegahan Kerentanan XSS (Cross-Site Scripting). -4. [#966](https://github.com/OpenSID/OpenKab/issues/966) Prevent IDOR (Insecure Direct Object Reference) pada Endpoint Berbasis ID. -5. [#968](https://github.com/OpenSID/OpenKab/issues/968) Jadikan Content Security Policy (CSP) Selalu Aktif, Tidak Boleh Auto-Disable Walau di Debug/Dev. -6. [#963](https://github.com/OpenSID/OpenKab/issues/963) Enforce Strong Password Policy di Seluruh Fitur (Change/Reset/Registration). -7. [#996](https://github.com/OpenSID/OpenKab/issues/996) Perbaikan parameter identitas OpenKab agar API hanya mengembalikan data sesuai dengan Kabupaten yang bersangkutan -1. [#369](https://github.com/OpenSID/API-Database-Gabungan/issues/369) Perbaikan cache artikel tidak dihapus setelah operasi hapus. #### Perubahan Teknis diff --git a/resources/views/data_pokok/data_presisi/pangan/detail_data.blade.php b/resources/views/data_pokok/data_presisi/pangan/detail_data.blade.php index e4c07119..cbdf7b56 100644 --- a/resources/views/data_pokok/data_presisi/pangan/detail_data.blade.php +++ b/resources/views/data_pokok/data_presisi/pangan/detail_data.blade.php @@ -59,7 +59,7 @@ @section('js') +@endsection diff --git a/resources/views/presisi/statistik/sandang.blade.php b/resources/views/presisi/statistik/sandang.blade.php index b30fa8c1..66cae6dd 100644 --- a/resources/views/presisi/statistik/sandang.blade.php +++ b/resources/views/presisi/statistik/sandang.blade.php @@ -9,365 +9,367 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ +
+
-
- -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + -@endsection -@push('css') - + $(document).on('click', '#reset', function(e) { + e.preventDefault(); + statistik.ajax.reload(); + }); + }); + +@endsection +@push('css') + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 86800f9a..3572c6aa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -35,7 +35,6 @@ use App\Http\Middleware\KabupatenMiddleware; use App\Http\Middleware\KecamatanMiddleware; use App\Http\Middleware\WilayahMiddleware; -use App\Http\Middleware\CheckPasswordExpiry; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; @@ -333,6 +332,11 @@ }) ->middleware(['permission:datapresisi-pangan-read']); + Route::prefix('sandang')->group(function () { + Route::get('detail_data', [App\Http\Controllers\DataPresisiSandangController::class, 'detailData'])->name('data-pokok.data-presisi-sandang.detail_data'); + }) + ->middleware(['permission:datapresisi-sandang-read']); + Route::prefix('adat')->group(function () { Route::get('/', [App\Http\Controllers\DataPresisiAdatController::class, 'index'])->name('data-pokok.data-presisi-adat.index'); Route::get('/detail', [App\Http\Controllers\DataPresisiAdatController::class, 'detail'])->name('data-pokok.data-presisi-adat.detail'); diff --git a/tests/Feature/DataPresisiSandangControllerTest.php b/tests/Feature/DataPresisiSandangControllerTest.php new file mode 100644 index 00000000..7ff28442 --- /dev/null +++ b/tests/Feature/DataPresisiSandangControllerTest.php @@ -0,0 +1,65 @@ +controller = new DataPresisiSandangController; + } + + public function test_detail_data_without_filter_returns_correct_view(): void + { + $response = $this->controller->detailData(new Request()); + + $this->assertInstanceOf(View::class, $response); + $this->assertEquals('data_pokok.data_presisi.sandang.detail_data', $response->name()); + + $data = $response->getData(); + $this->assertArrayHasKey('title', $data); + $this->assertArrayHasKey('colomn', $data); + $this->assertEquals('Data Presisi Sandang - ', $data['title']); + $this->assertEquals('', $data['colomn']); + } + + public function test_detail_data_with_filter_returns_correct_view(): void + { + $response = $this->controller->detailData(new Request([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + 'nilai' => 'pakaian', + ], + ])); + + $this->assertInstanceOf(View::class, $response); + + $data = $response->getData(); + $this->assertEquals('Data Presisi Sandang - Test Judul', $data['title']); + $this->assertEquals('kategori:pakaian', $data['colomn']); + } + + public function test_detail_data_with_partial_filter_returns_empty_colomn(): void + { + request()->merge([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + ], + ]); + + $response = $this->controller->detailData(new Request()); + + $data = $response->getData(); + $this->assertEquals('', $data['colomn']); + } +} From d5f48bd07afaf9960fe35796014477094f450054 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 6 May 2026 09:57:50 +0700 Subject: [PATCH 03/19] Merge Pull Request Feat: detail statistik papan (#1016) * simpan dulu * feat: detail presisi papan * tambahkan test * perbaikan mengikuti rekomendasi AI review * perbaikan filter tahun --------- Co-authored-by: Ahmad Affandi --- .gitignore | 1 + .../DataPresisiPapanController.php | 29 + .../Controllers/StatistikPapanController.php | 2 +- .../data_presisi/papan/detail_data.blade.php | 184 +++++ .../views/presisi/statistik/papan.blade.php | 756 +++++++++--------- routes/web.php | 5 + .../DataPresisiPapanControllerTest.php | 40 + 7 files changed, 637 insertions(+), 380 deletions(-) create mode 100644 app/Http/Controllers/DataPresisiPapanController.php create mode 100644 resources/views/data_pokok/data_presisi/papan/detail_data.blade.php create mode 100644 tests/Feature/DataPresisiPapanControllerTest.php diff --git a/.gitignore b/.gitignore index 5615236f..0e7b63b7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ package-lock.json /playwright-report/ .env.e2e /template_ai/ +AGENTS.md diff --git a/app/Http/Controllers/DataPresisiPapanController.php b/app/Http/Controllers/DataPresisiPapanController.php new file mode 100644 index 00000000..3f4450e4 --- /dev/null +++ b/app/Http/Controllers/DataPresisiPapanController.php @@ -0,0 +1,29 @@ +input('judul'); + $title = htmlspecialchars(strip_tags($title), ENT_QUOTES, 'UTF-8'); + $filter = $request->input('filter'); + + if (isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] && $filter['nilai']) { + $colomn = $filter['tipe'] . ':' . $filter['nilai']; + } + + return view('data_pokok.data_presisi.papan.detail_data', compact('title', 'colomn')); + } +} diff --git a/app/Http/Controllers/StatistikPapanController.php b/app/Http/Controllers/StatistikPapanController.php index cab69a9f..2dc5bf78 100644 --- a/app/Http/Controllers/StatistikPapanController.php +++ b/app/Http/Controllers/StatistikPapanController.php @@ -7,7 +7,7 @@ class StatistikPapanController extends Controller public function index() { return view('presisi.statistik.papan', [ - 'detailLink' => url(''), + 'detailLink' => url('data-presisi/papan/detail_data'), 'judul' => 'Papan' ]); } diff --git a/resources/views/data_pokok/data_presisi/papan/detail_data.blade.php b/resources/views/data_pokok/data_presisi/papan/detail_data.blade.php new file mode 100644 index 00000000..08efef40 --- /dev/null +++ b/resources/views/data_pokok/data_presisi/papan/detail_data.blade.php @@ -0,0 +1,184 @@ +@extends('layouts.index') + +@section('title', $title) + +@section('content_header') +

{{ html_entity_decode($title) }}

+@stop + +@section('content') +@include('partials.breadcrumbs') + +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
NONIKNOMOR KKNAMASTATUS KEPEMILIKANLUAS LANTAI (M²)JENIS LANTAIJENIS DINDINGSUMBER AIR MINUMSUMBER PENERANGANDAYA TERPASANGDAYA TERPASANG 2DAYA TERPASANG 3TANGGAL PENGISIANSTATUS PENGISIAN
+
+
+
+
+
+@endsection + +@section('js') + +@endsection \ No newline at end of file diff --git a/resources/views/presisi/statistik/papan.blade.php b/resources/views/presisi/statistik/papan.blade.php index f51af8e3..50c1d0df 100644 --- a/resources/views/presisi/statistik/papan.blade.php +++ b/resources/views/presisi/statistik/papan.blade.php @@ -9,365 +9,364 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ +
+
-
- -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 3572c6aa..d3e882cf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -331,6 +331,11 @@ Route::get('cetak', [App\Http\Controllers\DataPresisiPanganController::class, 'cetak'])->name('data-pokok.data-presisi-pangan.cetak'); }) ->middleware(['permission:datapresisi-pangan-read']); + + Route::prefix('papan')->group(function () { + Route::get('detail_data', [App\Http\Controllers\DataPresisiPapanController::class, 'detailData'])->name('data-pokok.data-presisi-papan.detail_data'); + }) + ->middleware(['permission:datapresisi-papan-read']); Route::prefix('sandang')->group(function () { Route::get('detail_data', [App\Http\Controllers\DataPresisiSandangController::class, 'detailData'])->name('data-pokok.data-presisi-sandang.detail_data'); diff --git a/tests/Feature/DataPresisiPapanControllerTest.php b/tests/Feature/DataPresisiPapanControllerTest.php new file mode 100644 index 00000000..4a7ff597 --- /dev/null +++ b/tests/Feature/DataPresisiPapanControllerTest.php @@ -0,0 +1,40 @@ +actingAsAdmin($user) + ->get(route('data-pokok.data-presisi-papan.detail_data', [ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'test_tipe', + 'nilai' => 'test_nilai' + ] + ])); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.papan.detail_data'); + } + + public function test_detail_data_returns_success_without_filter() + { + $user = User::first(); + $response = $this->actingAsAdmin($user) + ->get(route('data-pokok.data-presisi-papan.detail_data', [ + 'judul' => 'Test Judul Tanpa Filter' + ])); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.papan.detail_data'); + } +} \ No newline at end of file From 40e6c2f027ca2191f79e1431d7a90423cdfd3ec2 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 6 May 2026 10:07:35 +0700 Subject: [PATCH 04/19] Merge Pull Request From feat: detail statistik pendidikan (#1017) * feat: detail statistik pendidikan * perbaikan sesuai rekomendasi AI review * perbaikan test * perbaikan filter tahun * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- .../DataPresisiPendidikanController.php | 45 +- .../StatistikPendidikanController.php | 15 +- .../DetailDataPresisiPendidikanRequest.php | 31 + catatan_rilis.md | 2 + .../pendidikan/detail_data.blade.php | 157 ++++ .../presisi/statistik/pendidikan.blade.php | 761 +++++++++--------- routes/web.php | 1 + .../DataPresisiPendidikanControllerTest.php | 85 ++ 8 files changed, 708 insertions(+), 389 deletions(-) create mode 100644 app/Http/Requests/DetailDataPresisiPendidikanRequest.php create mode 100644 resources/views/data_pokok/data_presisi/pendidikan/detail_data.blade.php create mode 100644 tests/Feature/DataPresisiPendidikanControllerTest.php diff --git a/app/Http/Controllers/DataPresisiPendidikanController.php b/app/Http/Controllers/DataPresisiPendidikanController.php index 247b9ce8..1e6ce82c 100644 --- a/app/Http/Controllers/DataPresisiPendidikanController.php +++ b/app/Http/Controllers/DataPresisiPendidikanController.php @@ -2,25 +2,64 @@ namespace App\Http\Controllers; +use App\Http\Requests\DetailDataPresisiPendidikanRequest; use Illuminate\Http\Request; +use Illuminate\View\View; class DataPresisiPendidikanController extends Controller { - public function index() + /** + * Show the data presisi pendidikan index page. + * + * @return \Illuminate\View\View + */ + public function index(): View { $title = 'Data Presisi Pendidikan'; return view('data_pokok.data_presisi.pendidikan.index', compact('title')); } - public function detail(Request $request) + /** + * Display detail data presisi pendidikan. + * + * @param Request $request HTTP request instance + * @return View + */ + public function detail(Request $request): View { $data = json_decode($request->data); return view('data_pokok.data_presisi.pendidikan.detail', ['data' => $data]); } - public function cetak(Request $request) + /** + * Display detail data presisi pendidikan with optional filter. + * + * @param DetailDataPresisiPendidikanRequest $request HTTP request instance + * @return View + */ + public function detailData(DetailDataPresisiPendidikanRequest $request): View + { + $colomn = ''; + $title = 'Data Presisi Pendidikan - ' . $request->input('judul', ''); + $title = htmlspecialchars(strip_tags($title), ENT_QUOTES, 'UTF-8'); + $filter = $request->input('filter'); + + if (isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] && $filter['nilai']) { + $colomn = $filter['tipe'] . ':' . $filter['nilai']; + } + + return view('data_pokok.data_presisi.pendidikan.detail_data', compact('title', 'colomn')); + } + + /** + * Display print view for data presisi pendidikan. + * + * @param Request $request HTTP request instance + * @return View + */ + public function cetak(Request $request): View { return view('data_pokok.data_presisi.pendidikan.cetak', ['filter' => $request->getQueryString()]); } diff --git a/app/Http/Controllers/StatistikPendidikanController.php b/app/Http/Controllers/StatistikPendidikanController.php index eca9df10..3e5c88fa 100644 --- a/app/Http/Controllers/StatistikPendidikanController.php +++ b/app/Http/Controllers/StatistikPendidikanController.php @@ -2,13 +2,20 @@ namespace App\Http\Controllers; +use Illuminate\View\View; + class StatistikPendidikanController extends Controller { - public function index() + /** + * Display the statistics pendidikan page. + * + * @return View + */ + public function index(): View { return view('presisi.statistik.pendidikan', [ - 'detailLink' => url(''), - 'judul' => 'Pendidikan' + 'detailLink' => url('data-presisi/pendidikan/detail_data'), + 'judul' => 'Pendidikan', ]); - } + } } diff --git a/app/Http/Requests/DetailDataPresisiPendidikanRequest.php b/app/Http/Requests/DetailDataPresisiPendidikanRequest.php new file mode 100644 index 00000000..a7215c1b --- /dev/null +++ b/app/Http/Requests/DetailDataPresisiPendidikanRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'judul' => 'nullable|string|max:255', + 'filter' => 'nullable|array', + 'filter.tipe' => 'required_with:filter|string', + 'filter.nilai' => 'required_with:filter|string', + ]; + } +} diff --git a/catatan_rilis.md b/catatan_rilis.md index 6accf2d5..fa1d9491 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -4,6 +4,8 @@ Di rilis ini, versi 2604.0.1 berisi penambahan dan perbaikan yang diminta penggu 1. [#997](https://github.com/OpenSID/OpenKab/issues/997) Buat Halaman Detail Pangan di Statistik Presisi 2. [#1003](https://github.com/OpenSID/OpenKab/issues/1003) Buat Halaman Detail Sandang di Statistik Presisi +2. [#1004](https://github.com/OpenSID/OpenKab/issues/1004) Buat Halaman Detail Papan di Statistik Presisi +2. [#1002](https://github.com/OpenSID/OpenKab/issues/1002) Buat Halaman Detail Pendidikan di Statistik Presisi diff --git a/resources/views/data_pokok/data_presisi/pendidikan/detail_data.blade.php b/resources/views/data_pokok/data_presisi/pendidikan/detail_data.blade.php new file mode 100644 index 00000000..6dc97d97 --- /dev/null +++ b/resources/views/data_pokok/data_presisi/pendidikan/detail_data.blade.php @@ -0,0 +1,157 @@ +@extends('layouts.index') + +@section('title', $title) + +@section('content_header') +

{{ html_entity_decode($title) }}

+@stop + +@section('content') +@include('partials.breadcrumbs') + +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + +
NONIKNOMOR KKNAMATINGKAT PENDIDIKAN YANG DI TAMATKANJENJANG PENDIDIKAN YANG SEDANG DITEMPUHKEIKUTSERTAAN KIPJENIS PENDIDIKAN KESETARAAN YANG DIIKUTTANGGAL PENGISIANSTATUS PENGISIAN
+
+
+
+
+
+@endsection + +@section('js') + +@endsection diff --git a/resources/views/presisi/statistik/pendidikan.blade.php b/resources/views/presisi/statistik/pendidikan.blade.php index 67210cbe..f73431ba 100644 --- a/resources/views/presisi/statistik/pendidikan.blade.php +++ b/resources/views/presisi/statistik/pendidikan.blade.php @@ -9,365 +9,365 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ +
+
-
- -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index d3e882cf..89a04e22 100644 --- a/routes/web.php +++ b/routes/web.php @@ -320,6 +320,7 @@ Route::prefix('pendidikan')->group(function () { Route::get('/', [App\Http\Controllers\DataPresisiPendidikanController::class, 'index'])->name('data-pokok.data-presisi-pendidikan.index'); Route::get('/detail', [App\Http\Controllers\DataPresisiPendidikanController::class, 'detail'])->name('data-pokok.data-presisi-pendidikan.detail'); + Route::get('detail_data', [App\Http\Controllers\DataPresisiPendidikanController::class, 'detailData'])->name('data-pokok.data-presisi-pendidikan.detail_data'); Route::get('cetak', [App\Http\Controllers\DataPresisiPendidikanController::class, 'cetak'])->name('data-pokok.data-presisi-pendidikan.cetak'); }) ->middleware(['permission:datapresisi-pendidikan-read']); diff --git a/tests/Feature/DataPresisiPendidikanControllerTest.php b/tests/Feature/DataPresisiPendidikanControllerTest.php new file mode 100644 index 00000000..7419ba30 --- /dev/null +++ b/tests/Feature/DataPresisiPendidikanControllerTest.php @@ -0,0 +1,85 @@ +controller = new DataPresisiPendidikanController; + } + + private function createRequest(array $data = []): DetailDataPresisiPendidikanRequest + { + $request = app()->make(DetailDataPresisiPendidikanRequest::class); + $request->merge($data); + return $request; + } + + public function test_detail_data_without_filter_returns_correct_view(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => '', + ])); + + $this->assertInstanceOf(View::class, $response); + $this->assertEquals('data_pokok.data_presisi.pendidikan.detail_data', $response->name()); + + $data = $response->getData(); + $this->assertArrayHasKey('title', $data); + $this->assertArrayHasKey('colomn', $data); + $this->assertEquals('Data Presisi Pendidikan - ', $data['title']); + $this->assertEquals('', $data['colomn']); + } + + public function test_detail_data_with_filter_returns_correct_view(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + 'nilai' => 'pendidikan', + ], + ])); + + $this->assertInstanceOf(View::class, $response); + + $data = $response->getData(); + $this->assertEquals('Data Presisi Pendidikan - Test Judul', $data['title']); + $this->assertEquals('kategori:pendidikan', $data['colomn']); + } + + public function test_detail_data_with_partial_filter_returns_empty_colomn(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + ], + ])); + + $data = $response->getData(); + $this->assertEquals('', $data['colomn']); + } + + public function test_detail_data_title_is_sanitized_against_xss(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test', + 'filter' => [], + ])); + + $data = $response->getData(); + $this->assertStringNotContainsString(' +@endsection diff --git a/resources/views/presisi/statistik/ketenagakerjaan.blade.php b/resources/views/presisi/statistik/ketenagakerjaan.blade.php index ed460468..7bbfc144 100644 --- a/resources/views/presisi/statistik/ketenagakerjaan.blade.php +++ b/resources/views/presisi/statistik/ketenagakerjaan.blade.php @@ -9,364 +9,364 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ +
+
-
- -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + $(document).on('click', '#reset', function(e) { + e.preventDefault(); + statistik.ajax.reload(); + }); + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/resources/views/presisi/statistik/pendidikan.blade.php b/resources/views/presisi/statistik/pendidikan.blade.php index f73431ba..30481ce3 100644 --- a/resources/views/presisi/statistik/pendidikan.blade.php +++ b/resources/views/presisi/statistik/pendidikan.blade.php @@ -475,4 +475,4 @@ className: 'dt-body-right', color: blue; } -@endpush \ No newline at end of file +@endpush diff --git a/routes/web.php b/routes/web.php index 89a04e22..51f17949 100644 --- a/routes/web.php +++ b/routes/web.php @@ -313,6 +313,7 @@ Route::prefix('ketenagakerjaan')->group(function () { Route::get('/', [App\Http\Controllers\DataPresisiKetenagakerjaanController::class, 'index'])->name('data-pokok.data-presisi-ketenagakerjaan.index'); Route::get('/detail', [App\Http\Controllers\DataPresisiKetenagakerjaanController::class, 'detail'])->name('data-pokok.data-presisi-ketenagakerjaan.detail'); + Route::get('detail_data', [App\Http\Controllers\DataPresisiKetenagakerjaanController::class, 'detailData'])->name('data-pokok.data-presisi-ketenagakerjaan.detail_data'); Route::get('cetak', [App\Http\Controllers\DataPresisiKetenagakerjaanController::class, 'cetak'])->name('data-pokok.data-presisi-ketenagakerjaan.cetak'); }) ->middleware(['permission:datapresisi-ketenagakerjaan-read']); diff --git a/tests/Feature/DataPresisiKetenagakerjaanControllerTest.php b/tests/Feature/DataPresisiKetenagakerjaanControllerTest.php new file mode 100644 index 00000000..ace04f2d --- /dev/null +++ b/tests/Feature/DataPresisiKetenagakerjaanControllerTest.php @@ -0,0 +1,509 @@ +get(route('data-pokok.data-presisi-ketenagakerjaan.index')); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.ketenagakerjaan.index'); + } + + /** @test */ + public function test_index_page_has_correct_title(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.index')); + + $response->assertViewHas('title', 'Data Presisi Ketenagakerjaan'); + } + + /** @test */ + public function test_index_page_contains_required_elements(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.index')); + + $content = $response->getContent(); + + // Test DataTable exists + $this->assertStringContainsString('id="table-ketenagakerjaan"', $content); + + // Test chart canvas exists + $this->assertStringContainsString('id="barChart"', $content); + + // Test filter tahun exists + $this->assertStringContainsString('filter-tahun', $content); + } + + /** @test */ + public function test_index_page_has_table_headers(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.index')); + + $content = $response->getContent(); + + $expectedHeaders = [ + 'Aksi', + 'NIK', + 'Nama Kepala Keluarga', + 'Jumlah Anggota RTM', + 'Jenis Pekerjaan', + 'Tempat Kerja', + ]; + + foreach ($expectedHeaders as $header) { + $this->assertStringContainsString($header, $content); + } + } + + /** @test */ + public function test_index_page_has_excel_download_button(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.index')); + + $content = $response->getContent(); + + $this->assertStringContainsString('download-excel', $content); + $this->assertStringContainsString('table-ketenagakerjaan', $content); + $this->assertStringContainsString('/api/v1/data-presisi/ketenagakerjaan/rtm/download', $content); + $this->assertStringContainsString('data_presisi_ketenagakerjaan', $content); + } + + /** @test */ + public function test_index_page_has_print_button(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.index')); + + $content = $response->getContent(); + + $this->assertStringContainsString('ketenagakerjaan/cetak', $content); + } + + // ========================================== + // Detail Tests + // ========================================== + + /** @test */ + public function test_can_access_ketenagakerjaan_detail_page(): void + { + $data = json_encode([ + 'rtm_id' => '1', + 'no_kartu_rumah' => '1234567890', + 'nama_kepala_keluarga' => 'John Doe', + 'alamat' => 'Jl. Test No. 1', + 'jumlah_anggota' => 4, + 'jumlah_kk' => 1, + ]); + + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail', ['data' => $data])); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.ketenagakerjaan.detail'); + } + + /** @test */ + public function test_detail_page_has_decoded_json_data(): void + { + $data = json_encode([ + 'rtm_id' => '1', + 'no_kartu_rumah' => '1234567890', + 'nama_kepala_keluarga' => 'John Doe', + 'alamat' => 'Jl. Test No. 1', + 'jumlah_anggota' => 4, + 'jumlah_kk' => 1, + ]); + + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail', ['data' => $data])); + + $response->assertViewHas('data'); + $viewData = $response->viewData('data'); + + $this->assertEquals('1', $viewData->rtm_id); + $this->assertEquals('1234567890', $viewData->no_kartu_rumah); + $this->assertEquals('John Doe', $viewData->nama_kepala_keluarga); + $this->assertEquals('Jl. Test No. 1', $viewData->alamat); + $this->assertEquals(4, $viewData->jumlah_anggota); + $this->assertEquals(1, $viewData->jumlah_kk); + } + + /** @test */ + public function test_detail_page_displays_kepala_keluarga_name(): void + { + $data = json_encode([ + 'rtm_id' => '1', + 'no_kartu_rumah' => '1234567890', + 'nama_kepala_keluarga' => 'Jane Doe', + 'alamat' => 'Jl. Test', + 'jumlah_anggota' => 3, + 'jumlah_kk' => 1, + ]); + + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail', ['data' => $data])); + + $content = $response->getContent(); + + $this->assertStringContainsString('Jane Doe', $content); + $this->assertStringContainsString('1234567890', $content); + } + + /** @test */ + public function test_detail_page_has_detail_table(): void + { + $data = json_encode([ + 'rtm_id' => '1', + 'no_kartu_rumah' => '1234567890', + 'nama_kepala_keluarga' => 'John Doe', + 'alamat' => 'Jl. Test', + 'jumlah_anggota' => 3, + 'jumlah_kk' => 1, + ]); + + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail', ['data' => $data])); + + $content = $response->getContent(); + + $this->assertStringContainsString('id="detail-ketenagakerjaan"', $content); + } + + /** @test */ + public function test_detail_page_has_back_link_to_index(): void + { + $data = json_encode([ + 'rtm_id' => '1', + 'no_kartu_rumah' => '1234567890', + 'nama_kepala_keluarga' => 'John Doe', + 'alamat' => 'Jl. Test', + 'jumlah_anggota' => 3, + 'jumlah_kk' => 1, + ]); + + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail', ['data' => $data])); + + $content = $response->getContent(); + + $this->assertStringContainsString(route('data-pokok.data-presisi-ketenagakerjaan.index'), $content); + } + + // ========================================== + // Cetak Tests + // ========================================== + + /** @test */ + public function test_can_access_ketenagakerjaan_cetak_page(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.cetak')); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.ketenagakerjaan.cetak'); + } + + /** @test */ + public function test_cetak_page_has_filter_view_variable(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.cetak')); + + $response->assertViewHas('filter'); + } + + /** @test */ + public function test_cetak_page_passes_query_string_as_filter(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.cetak') . '?tahun=2024&status=active'); + + $response->assertViewHas('filter'); + $filter = $response->viewData('filter'); + + $this->assertStringContainsString('tahun=2024', $filter); + $this->assertStringContainsString('status=active', $filter); + } + + /** @test */ + public function test_cetak_page_has_table_with_correct_id(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.cetak')); + + $content = $response->getContent(); + + $this->assertStringContainsString('id="tabel-ketenagakerjaan"', $content); + } + + /** @test */ + public function test_cetak_page_has_correct_table_headers(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.cetak')); + + $content = $response->getContent(); + + $expectedHeaders = [ + 'NO', + 'NIK', + 'NAMA KEPALA KELUARGA', + 'JUMLAH ANGGOTA', + 'JENIS PEKERJAAN', + 'TEMPAT KERJA', + 'FREKWENSI MENGIKUTI PELATIHAN SETAHUN', + 'JENIS PELATIHAN DIIKUTI SETAHUN', + 'TANGGAL PENGISIAN', + 'STATUS PENGISIAN', + ]; + + foreach ($expectedHeaders as $header) { + $this->assertStringContainsString($header, $content); + } + } + + /** @test */ + public function test_cetak_page_uses_cetak_layout(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.cetak')); + + $response->assertStatus(200); + $content = $response->getContent(); + + $this->assertStringContainsString('Cetak | Data Penduduk Data Presisi Ketenagakerjaan', $content); + } + + // ========================================== + // DetailData Tests + // ========================================== + + /** @test */ + public function test_can_access_ketenagakerjaan_detail_data_page(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.ketenagakerjaan.detail_data'); + } + + /** @test */ + public function test_detail_data_page_has_title_and_colomn(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $response->assertViewHas('title'); + $response->assertViewHas('colomn'); + } + + /** @test */ + public function test_detail_data_default_title_without_judul(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $title = $response->viewData('title'); + + $this->assertEquals('Data Presisi Ketenagakerjaan - ', $title); + } + + /** @test */ + public function test_detail_data_title_includes_judul(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', ['judul' => 'Test Judul'])); + + $title = $response->viewData('title'); + + $this->assertStringContainsString('Test Judul', $title); + } + + /** @test */ + public function test_detail_data_title_sanitizes_xss(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', ['judul' => ''])); + + $title = $response->viewData('title'); + + $this->assertStringNotContainsString('', $title); + } + + /** @test */ + public function test_detail_data_colomn_empty_without_filter(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_colomn_with_valid_filter(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'tipe' => 'jenis_pekerjaan', + 'nilai' => 'petani', + ], + ])); + + $colomn = $response->viewData('colomn'); + + $this->assertEquals('jenis_pekerjaan:petani', $colomn); + } + + /** @test */ + public function test_detail_data_colomn_empty_when_tipe_missing(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'nilai' => 'petani', + ], + ])); + + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_colomn_empty_when_nilai_missing(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'tipe' => 'jenis_pekerjaan', + ], + ])); + + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_colomn_empty_when_tipe_empty_string(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'tipe' => '', + 'nilai' => 'petani', + ], + ])); + + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_colomn_empty_when_nilai_empty_string(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'tipe' => 'jenis_pekerjaan', + 'nilai' => '', + ], + ])); + + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_page_has_detail_table(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $content = $response->getContent(); + + $this->assertStringContainsString('id="detail-ketenagakerjaan"', $content); + } + + /** @test */ + public function test_detail_data_page_has_filter_tahun_component(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $content = $response->getContent(); + + $this->assertStringContainsString('filter-tahun', $content); + } + + /** @test */ + public function test_detail_data_page_has_correct_table_headers(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data')); + + $content = $response->getContent(); + + $expectedHeaders = [ + 'NO', + 'NIK', + 'NOMOR KK', + 'NAMA', + 'PEKERJAAN', + 'TEMPAT KERJA', + 'FREKWENSI MENGIKUTI PELATIHAN SETAHUN', + 'JENIS PELATIHAN YANG DIIKUTI SETAHUN', + 'TANGGAL PENGISIAN', + 'STATUS PENGISIAN', + ]; + + foreach ($expectedHeaders as $header) { + $this->assertStringContainsString($header, $content); + } + } + + /** @test */ + public function test_detail_data_validation_requires_filter_tipe_when_filter_nilai_present(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'nilai' => 'petani', + ], + ])); + + $response->assertStatus(200); + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_validation_requires_filter_nilai_when_filter_tipe_present(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', [ + 'filter' => [ + 'tipe' => 'jenis_pekerjaan', + ], + ])); + + $response->assertStatus(200); + $colomn = $response->viewData('colomn'); + + $this->assertEquals('', $colomn); + } + + /** @test */ + public function test_detail_data_title_strips_html_tags(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', ['judul' => 'Bold Italic'])); + + $title = $response->viewData('title'); + + $this->assertStringNotContainsString('', $title); + $this->assertStringNotContainsString('', $title); + $this->assertStringContainsString('Bold', $title); + $this->assertStringContainsString('Italic', $title); + } + + /** @test */ + public function test_detail_data_title_encodes_special_characters(): void + { + $response = $this->get(route('data-pokok.data-presisi-ketenagakerjaan.detail_data', ['judul' => 'Test "Quotes" & '])); + + $title = $response->viewData('title'); + + $this->assertStringNotContainsString('', $title); + $this->assertStringContainsString('&', $title); + } +} From 40322037d073bb39a219bb0e2776a882822d45d0 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 6 May 2026 10:30:34 +0700 Subject: [PATCH 06/19] Merge Pull Request From feat: detail statistik keagamaan (#1020) * feat: detail statistik keagamaan * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- ...ataPresisiAktivitasKeagamaanController.php | 27 + .../StatistikAktivitasKeagamaanController.php | 8 +- ...ilDataPresisiAktivitasKeagamaanRequest.php | 31 + catatan_rilis.md | 1 + .../aktivitas_keagamaan/detail_data.blade.php | 142 ++++ .../statistik/aktivitas-keagamaan.blade.php | 755 +++++++++--------- routes/web.php | 5 + ...taPresisiAktivitasKeagamaanRequestTest.php | 69 ++ ...resisiAktivitasKeagamaanControllerTest.php | 65 ++ 9 files changed, 722 insertions(+), 381 deletions(-) create mode 100644 app/Http/Controllers/DataPresisiAktivitasKeagamaanController.php create mode 100644 app/Http/Requests/DetailDataPresisiAktivitasKeagamaanRequest.php create mode 100644 resources/views/data_pokok/data_presisi/aktivitas_keagamaan/detail_data.blade.php create mode 100644 tests/Feature/DetailDataPresisiAktivitasKeagamaanRequestTest.php create mode 100644 tests/Feature/Http/Controllers/DataPresisiAktivitasKeagamaanControllerTest.php diff --git a/app/Http/Controllers/DataPresisiAktivitasKeagamaanController.php b/app/Http/Controllers/DataPresisiAktivitasKeagamaanController.php new file mode 100644 index 00000000..bd610f64 --- /dev/null +++ b/app/Http/Controllers/DataPresisiAktivitasKeagamaanController.php @@ -0,0 +1,27 @@ +input('judul', ''); + $title = htmlspecialchars(strip_tags($judul)); + + $filter = $request->input('filter', []); + $colomn = ''; + + if ($request->filled('filter.tipe') && $request->filled('filter.nilai')) { + $colomn = $filter['tipe'] . ':' . $filter['nilai']; + } + + return view('data_pokok.data_presisi.aktivitas_keagamaan.detail_data', [ + 'title' => $title, + 'colomn' => $colomn + ]); + } +} diff --git a/app/Http/Controllers/StatistikAktivitasKeagamaanController.php b/app/Http/Controllers/StatistikAktivitasKeagamaanController.php index 40016e56..85ce8f9c 100644 --- a/app/Http/Controllers/StatistikAktivitasKeagamaanController.php +++ b/app/Http/Controllers/StatistikAktivitasKeagamaanController.php @@ -2,13 +2,15 @@ namespace App\Http\Controllers; +use Illuminate\View\View; + class StatistikAktivitasKeagamaanController extends Controller { - public function index() + public function index(): View { return view('presisi.statistik.aktivitas-keagamaan', [ - 'detailLink' => url(''), - 'judul' => 'Aktivitas Keagamaan' + 'detailLink' => url('data-presisi/aktivitas-keagamaan/detail_data'), + 'judul' => 'Aktivitas Keagamaan' ]); } } diff --git a/app/Http/Requests/DetailDataPresisiAktivitasKeagamaanRequest.php b/app/Http/Requests/DetailDataPresisiAktivitasKeagamaanRequest.php new file mode 100644 index 00000000..5acdb797 --- /dev/null +++ b/app/Http/Requests/DetailDataPresisiAktivitasKeagamaanRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'judul' => 'nullable|string|max:255', + 'filter' => 'nullable|array', + 'filter.tipe' => 'required_with:filter|string|in:agama_id,frekwensi_mengikuti_kegiatan_setahun', + 'filter.nilai' => 'required_with:filter|string', + ]; + } +} diff --git a/catatan_rilis.md b/catatan_rilis.md index c39313b5..68063dfe 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -7,6 +7,7 @@ Di rilis ini, versi 2604.0.1 berisi penambahan dan perbaikan yang diminta penggu 3. [#1004](https://github.com/OpenSID/OpenKab/issues/1004) Buat Halaman Detail Papan di Statistik Presisi 4. [#1002](https://github.com/OpenSID/OpenKab/issues/1002) Buat Halaman Detail Pendidikan di Statistik Presisi 5. [#1001](https://github.com/OpenSID/OpenKab/issues/1001) Buat Halaman Detail Ketenagakerjaan di Statistik Presisi +6. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Keagamaan di Statistik Presisi diff --git a/resources/views/data_pokok/data_presisi/aktivitas_keagamaan/detail_data.blade.php b/resources/views/data_pokok/data_presisi/aktivitas_keagamaan/detail_data.blade.php new file mode 100644 index 00000000..024be3b4 --- /dev/null +++ b/resources/views/data_pokok/data_presisi/aktivitas_keagamaan/detail_data.blade.php @@ -0,0 +1,142 @@ +@extends('layouts.index') + +@section('title', $title) + +@section('content_header') +

{{ $title }}

+@stop + +@section('content') +@include('partials.breadcrumbs') + +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + +
NONIKNOMOR KKNAMAAGAMAFREKWENSI AKTIVITASTANGGAL PENGISIANSTATUS PENGISIAN
+
+
+
+
+
+@endsection + +@section('js') + +@endsection \ No newline at end of file diff --git a/resources/views/presisi/statistik/aktivitas-keagamaan.blade.php b/resources/views/presisi/statistik/aktivitas-keagamaan.blade.php index 3c80570c..ff285dcc 100644 --- a/resources/views/presisi/statistik/aktivitas-keagamaan.blade.php +++ b/resources/views/presisi/statistik/aktivitas-keagamaan.blade.php @@ -9,364 +9,364 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ +
+
-
- -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + $(document).on('click', '#reset', function(e) { + e.preventDefault(); + statistik.ajax.reload(); + }); + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 51f17949..0b78f135 100644 --- a/routes/web.php +++ b/routes/web.php @@ -318,6 +318,11 @@ }) ->middleware(['permission:datapresisi-ketenagakerjaan-read']); + Route::prefix('aktivitas-keagamaan')->group(function () { + Route::get('detail_data', [App\Http\Controllers\DataPresisiAktivitasKeagamaanController::class, 'detailData'])->name('data-pokok.data-presisi-aktivitas-keagamaan.detail_data'); + }) + ->middleware(['permission:datapresisi-aktivitas-keagamaan-read']); + Route::prefix('pendidikan')->group(function () { Route::get('/', [App\Http\Controllers\DataPresisiPendidikanController::class, 'index'])->name('data-pokok.data-presisi-pendidikan.index'); Route::get('/detail', [App\Http\Controllers\DataPresisiPendidikanController::class, 'detail'])->name('data-pokok.data-presisi-pendidikan.detail'); diff --git a/tests/Feature/DetailDataPresisiAktivitasKeagamaanRequestTest.php b/tests/Feature/DetailDataPresisiAktivitasKeagamaanRequestTest.php new file mode 100644 index 00000000..83b0f84e --- /dev/null +++ b/tests/Feature/DetailDataPresisiAktivitasKeagamaanRequestTest.php @@ -0,0 +1,69 @@ +get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['judul' => 'Valid title'])); + $response->assertStatus(200); + + // Judul too long + $longJudul = str_repeat('a', 256); + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['judul' => $longJudul])); + $response->assertRedirect(); + $response->assertSessionHasErrors('judul'); + } + + /** @test */ + public function it_validates_filter_as_nullable_array() + { + // Valid filter + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['tipe' => 'agama_id', 'nilai' => '1']])); + $response->assertStatus(200); + + // Invalid filter - not array + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => 'not_array'])); + $response->assertRedirect(); + $response->assertSessionHasErrors('filter'); + } + + /** @test */ + public function it_validates_filter_tipe_required_with_filter_and_in_allowed_values() + { + // Valid tipe + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['tipe' => 'agama_id', 'nilai' => '1']])); + $response->assertStatus(200); + + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['tipe' => 'frekwensi_mengikuti_kegiatan_setahun', 'nilai' => '1']])); + $response->assertStatus(200); + + // Invalid tipe + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['tipe' => 'invalid_tipe', 'nilai' => '1']])); + $response->assertRedirect(); + $response->assertSessionHasErrors('filter.tipe'); + + // Missing tipe when filter present + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['nilai' => '1']])); + $response->assertRedirect(); + $response->assertSessionHasErrors('filter.tipe'); + } + + /** @test */ + public function it_validates_filter_nilai_required_with_filter_as_string() + { + // Valid nilai + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['tipe' => 'agama_id', 'nilai' => '1']])); + $response->assertStatus(200); + + // Missing nilai when filter present + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => ['tipe' => 'agama_id']])); + $response->assertRedirect(); + $response->assertSessionHasErrors('filter.nilai'); + } +} \ No newline at end of file diff --git a/tests/Feature/Http/Controllers/DataPresisiAktivitasKeagamaanControllerTest.php b/tests/Feature/Http/Controllers/DataPresisiAktivitasKeagamaanControllerTest.php new file mode 100644 index 00000000..d797a1b7 --- /dev/null +++ b/tests/Feature/Http/Controllers/DataPresisiAktivitasKeagamaanControllerTest.php @@ -0,0 +1,65 @@ +get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data')); + + $response->assertStatus(200) + ->assertViewIs('data_pokok.data_presisi.aktivitas_keagamaan.detail_data') + ->assertViewHas('title') + ->assertViewHas('colomn'); + } + + /** @test */ + public function it_handles_judul_parameter() + { + $judul = 'Test Title '; + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['judul' => $judul])); + + $response->assertStatus(200) + ->assertViewHas('title', htmlspecialchars(strip_tags($judul))) + ->assertViewHas('colomn', ''); + } + + /** @test */ + public function it_handles_filter_parameter() + { + $filter = [ + 'tipe' => 'agama_id', + 'nilai' => '1' + ]; + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => $filter])); + + $response->assertStatus(200) + ->assertViewHas('colomn', 'agama_id:1'); + } + + /** @test */ + public function it_handles_filter_with_different_tipe() + { + $filter = [ + 'tipe' => 'frekwensi_mengikuti_kegiatan_setahun', + 'nilai' => '2' + ]; + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => $filter])); + + $response->assertStatus(200) + ->assertViewHas('colomn', 'frekwensi_mengikuti_kegiatan_setahun:2'); + } + + /** @test */ + public function it_handles_empty_filter() + { + $response = $this->get(route('data-pokok.data-presisi-aktivitas-keagamaan.detail_data', ['filter' => []])); + + $response->assertStatus(200) + ->assertViewHas('colomn', ''); + } +} \ No newline at end of file From 9ae773c3d47bbc3935879c8b9342426f3425e7a4 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 6 May 2026 10:42:50 +0700 Subject: [PATCH 07/19] Merge Pull Request From fix: tinymce pada artikel (#1022) * fix: tinymce pada artikel * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- catatan_rilis.md | 1 + resources/views/layouts/index.blade.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/catatan_rilis.md b/catatan_rilis.md index 68063dfe..05ea7fc3 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -8,6 +8,7 @@ Di rilis ini, versi 2604.0.1 berisi penambahan dan perbaikan yang diminta penggu 4. [#1002](https://github.com/OpenSID/OpenKab/issues/1002) Buat Halaman Detail Pendidikan di Statistik Presisi 5. [#1001](https://github.com/OpenSID/OpenKab/issues/1001) Buat Halaman Detail Ketenagakerjaan di Statistik Presisi 6. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Keagamaan di Statistik Presisi +7. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor diff --git a/resources/views/layouts/index.blade.php b/resources/views/layouts/index.blade.php index e12192f8..b697cd76 100644 --- a/resources/views/layouts/index.blade.php +++ b/resources/views/layouts/index.blade.php @@ -1,5 +1,7 @@ @extends('adminlte::page') +@section('meta_tags') +@endsection @section('footer') Hak cipta © OpenDesa. Seluruh hak cipta dilindungi. From f1acc20002111efd89ff4f33a30212766dab5a51 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 6 May 2026 10:48:26 +0700 Subject: [PATCH 08/19] Merge Pull Request From fix: error ketika halaman login menampilkan captcha (#1024) * fix: error ketika halaman login menampilkan captcha * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- catatan_rilis.md | 2 +- composer.json | 1 + composer.lock | 81 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/catatan_rilis.md b/catatan_rilis.md index 05ea7fc3..94e4ac07 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -13,7 +13,7 @@ Di rilis ini, versi 2604.0.1 berisi penambahan dan perbaikan yang diminta penggu #### Perbaikan BUG - +1. [#1023](https://github.com/OpenSID/OpenKab/issues/1023) Percobaan login gagal terkadang error 500 #### Perubahan Teknis diff --git a/composer.json b/composer.json index ce932c50..2f1303c6 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "laravel/tinker": "^2.8", "laravel/ui": "^4.2", "league/flysystem-ftp": "^3.10", + "mews/captcha": "^3.3", "mews/purifier": "^3.4", "openspout/openspout": "^4.24", "proengsoft/laravel-jsvalidation": "^4.8", diff --git a/composer.lock b/composer.lock index 57f53cc7..b94addd3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e6c80fb59e61ffc48245d30a50a22485", + "content-hash": "e57d82cdf838e45b87bad3ade52b1f6c", "packages": [ { "name": "akaunting/laravel-apexcharts", @@ -3816,6 +3816,79 @@ }, "time": "2024-11-14T23:14:52+00:00" }, + { + "name": "mews/captcha", + "version": "3.3.3", + "source": { + "type": "git", + "url": "https://github.com/mewebstudio/captcha.git", + "reference": "e996a9a5638296de3e9dac41782dbdcf3d14ce11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mewebstudio/captcha/zipball/e996a9a5638296de3e9dac41782dbdcf3d14ce11", + "reference": "e996a9a5638296de3e9dac41782dbdcf3d14ce11", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "illuminate/config": "~5|^6|^7|^8|^9|^10|^11", + "illuminate/filesystem": "~5|^6|^7|^8|^9|^10|^11", + "illuminate/hashing": "~5|^6|^7|^8|^9|^10|^11", + "illuminate/session": "~5|^6|^7|^8|^9|^10|^11", + "illuminate/support": "~5|^6|^7|^8|^9|^10|^11", + "intervention/image": "~2.5", + "php": "^7.2|^8.1|^8.2|^8.3" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.5|^9.5.10|^10.5" + }, + "type": "package", + "extra": { + "laravel": { + "aliases": { + "Captcha": "Mews\\Captcha\\Facades\\Captcha" + }, + "providers": [ + "Mews\\Captcha\\CaptchaServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Mews\\Captcha\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muharrem ERİN", + "email": "me@mewebstudio.com", + "homepage": "https://github.com/mewebstudio", + "role": "Developer" + } + ], + "description": "Laravel 5/6/7/8/9/10/11 Captcha Package", + "homepage": "https://github.com/mewebstudio/captcha", + "keywords": [ + "captcha", + "laravel5 Security", + "laravel6 Captcha", + "laravel6 Security" + ], + "support": { + "issues": "https://github.com/mewebstudio/captcha/issues", + "source": "https://github.com/mewebstudio/captcha/tree/3.3.3" + }, + "time": "2024-03-20T16:15:48+00:00" + }, { "name": "mews/purifier", "version": "3.4.3", @@ -12493,12 +12566,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.1" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } From 461cebffa975f637701f51c484e49de5cd60f64f Mon Sep 17 00:00:00 2001 From: Habibie Mahbub <56639378+habibie11@users.noreply.github.com> Date: Wed, 6 May 2026 15:14:36 +0700 Subject: [PATCH 09/19] Merge Pull Request From Validasi kategori artikel ketika membuat artikel baru (#1027) * validasi kategori artikel ketika membuat artikel baru * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- .../Master/ArtikelKabupatenController.php | 4 +- catatan_rilis.md | 1 + .../views/master/artikel/create.blade.php | 71 +++++++++++++++++-- tests/Feature/ArtikelControllerTest.php | 12 ++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Master/ArtikelKabupatenController.php b/app/Http/Controllers/Master/ArtikelKabupatenController.php index eca38407..2b4fd257 100644 --- a/app/Http/Controllers/Master/ArtikelKabupatenController.php +++ b/app/Http/Controllers/Master/ArtikelKabupatenController.php @@ -32,7 +32,9 @@ public function index(Request $request): View */ public function create(): View { - return view('master.artikel.create'); + $listPermission = $this->generateListPermission(); + + return view('master.artikel.create')->with($listPermission); } /** diff --git a/catatan_rilis.md b/catatan_rilis.md index 94e4ac07..0bcf926d 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -14,6 +14,7 @@ Di rilis ini, versi 2604.0.1 berisi penambahan dan perbaikan yang diminta penggu #### Perbaikan BUG 1. [#1023](https://github.com/OpenSID/OpenKab/issues/1023) Percobaan login gagal terkadang error 500 +2. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid #### Perubahan Teknis diff --git a/resources/views/master/artikel/create.blade.php b/resources/views/master/artikel/create.blade.php index 2068eddb..dce6236d 100644 --- a/resources/views/master/artikel/create.blade.php +++ b/resources/views/master/artikel/create.blade.php @@ -68,6 +68,14 @@ class="btn col-12 btn-primary m-0 rounded-pill"> + + Kategori artikel belum tersedia. + @if (($canwrite ?? 0) == 1) + Buat kategori artikel sekarang. + @else + Silakan hubungi admin untuk menambahkan kategori. + @endif +
@@ -119,6 +127,8 @@ class="btn col-12 btn-primary m-0 rounded-pill"> @include('partials.asset_tinymce') `; + }; + + var global$1 = tinymce.util.Tools.resolve('tinymce.dom.ScriptLoader'); var global = tinymce.util.Tools.resolve('tinymce.util.Tools'); - const option = name => editor => editor.options.get(name); + const option = (name) => (editor) => editor.options.get(name); const getContentStyle = option('content_style'); const shouldUseContentCssCors = option('content_css_cors'); const getBodyClass = option('body_class'); const getBodyId = option('body_id'); - const getPreviewHtml = editor => { - var _a; - let headHtml = ''; - const encode = editor.dom.encode; - const contentStyle = (_a = getContentStyle(editor)) !== null && _a !== void 0 ? _a : ''; - headHtml += ''; - const cors = shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : ''; - global.each(editor.contentCSS, url => { - headHtml += ''; - }); - if (contentStyle) { - headHtml += ''; - } - const bodyId = getBodyId(editor); - const bodyClass = getBodyClass(editor); - const isMetaKeyPressed = global$1.os.isMacOS() || global$1.os.isiOS() ? 'e.metaKey' : 'e.ctrlKey && !e.altKey'; - const preventClicksOnLinksScript = ' '; - const directionality = editor.getBody().dir; - const dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : ''; - const previewHtml = '' + '' + '' + headHtml + '' + '' + editor.getContent() + preventClicksOnLinksScript + '' + ''; - return previewHtml; - }; - - const open = editor => { - const content = getPreviewHtml(editor); - const dataApi = editor.windowManager.open({ - title: 'Preview', - size: 'large', - body: { - type: 'panel', - items: [{ - name: 'preview', - type: 'iframe', - sandboxed: true, - transparent: false - }] - }, - buttons: [{ - type: 'cancel', - name: 'close', - text: 'Close', - primary: true - }], - initialData: { preview: content } - }); - dataApi.focus('close'); - }; - - const register$1 = editor => { - editor.addCommand('mcePreview', () => { - open(editor); - }); - }; - - const register = editor => { - const onAction = () => editor.execCommand('mcePreview'); - editor.ui.registry.addButton('preview', { - icon: 'preview', - tooltip: 'Preview', - onAction - }); - editor.ui.registry.addMenuItem('preview', { - icon: 'preview', - text: 'Preview', - onAction - }); + const getComponentScriptsHtml = (editor) => { + const urls = unique(values(editor.schema.getComponentUrls())); + return map(urls, (url) => { + const attrs = mapToArray(global$1.ScriptLoader.getScriptAttributes(url), (v, k) => ` ${editor.dom.encode(k)}="${editor.dom.encode(v)}"`); + return ``; + }).join(''); + }; + const getPreviewHtml = (editor, contentCssResources) => { + let headHtml = ''; + const encode = editor.dom.encode; + const contentStyle = getContentStyle(editor) ?? ''; + headHtml += ``; + const cors = shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : ''; + global.each(contentCssResources, (resource) => { + if (resource.type === 'bundled') { + headHtml += ''; + } + else { + headHtml += ''; + } + }); + if (contentStyle) { + headHtml += ''; + } + headHtml += getComponentScriptsHtml(editor); + const bodyId = getBodyId(editor); + const bodyClass = getBodyClass(editor); + const directionality = editor.getBody().dir; + const dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : ''; + const previewHtml = ('' + + '' + + '' + + headHtml + + '' + + '' + + editor.getContent() + + getPreventClicksOnLinksScript() + + '' + + ''); + return previewHtml; + }; + + const open = (editor, contentCssResources) => { + const content = getPreviewHtml(editor, contentCssResources); + const dataApi = editor.windowManager.open({ + title: 'Preview', + size: 'large', + body: { + type: 'panel', + items: [ + { + name: 'preview', + type: 'iframe', + sandboxed: true, + transparent: false + } + ] + }, + buttons: [ + { + type: 'cancel', + name: 'close', + text: 'Close', + primary: true + } + ], + initialData: { + preview: content + } + }); + // Focus the close button, as by default the first element in the body is selected + // which we don't want to happen here since the body only has the iframe content + dataApi.focus('close'); + }; + + const register$1 = (editor, getContentCssResources) => { + editor.addCommand('mcePreview', () => { + open(editor, getContentCssResources()); + }); + }; + + const register = (editor) => { + const onAction = () => editor.execCommand('mcePreview'); + editor.ui.registry.addButton('preview', { + icon: 'preview', + tooltip: 'Preview', + onAction, + context: 'any' + }); + editor.ui.registry.addMenuItem('preview', { + icon: 'preview', + text: 'Preview', + onAction, + context: 'any' + }); }; var Plugin = () => { - global$2.add('preview', editor => { - register$1(editor); - register(editor); - }); + global$2.add('preview', (editor) => { + const getContentCssResources = () => map(editor.contentCSS, (key) => Optional.from(tinymce.Resource.get(key)) + .filter(isString) + .map((content) => ({ type: 'bundled', content })) + .getOr({ type: 'link', url: editor.documentBaseURI.toAbsolute(key) })); + register$1(editor, getContentCssResources); + register(editor); + }); }; Plugin(); + /** ***** + * DO NOT EXPORT ANYTHING + * + * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE + *******/ })(); diff --git a/public/vendor/tinymce/plugins/preview/plugin.min.js b/public/vendor/tinymce/plugins/preview/plugin.min.js index da6f1fa9..4867cd73 100644 --- a/public/vendor/tinymce/plugins/preview/plugin.min.js +++ b/public/vendor/tinymce/plugins/preview/plugin.min.js @@ -1,4 +1 @@ -/** - * TinyMCE version 6.7.0 (2023-08-30) - */ -!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=e=>t=>t.options.get(e),i=n("content_style"),s=n("content_css_cors"),c=n("body_class"),r=n("body_id");e.add("preview",(e=>{(e=>{e.addCommand("mcePreview",(()=>{(e=>{const n=(e=>{var n;let l="";const a=e.dom.encode,d=null!==(n=i(e))&&void 0!==n?n:"";l+='';const m=s(e)?' crossorigin="anonymous"':"";o.each(e.contentCSS,(t=>{l+='"})),d&&(l+='");const y=r(e),u=c(e),v=' '; - const directionality = editor.getBody().dir; - const dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : ''; - previewHtml = '' + '' + '' + '' + contentCssEntries + preventClicksOnLinksScript + '' + '' + previewHtml + '' + ''; - } - return replaceTemplateValues(previewHtml, getPreviewReplaceValues(editor)); - }; - const open = (editor, templateList) => { - const createTemplates = () => { - if (!templateList || templateList.length === 0) { - const message = editor.translate('No templates defined.'); - editor.notificationManager.open({ - text: message, - type: 'info' - }); - return Optional.none(); - } - return Optional.from(global$2.map(templateList, (template, index) => { - const isUrlTemplate = t => t.url !== undefined; - return { - selected: index === 0, - text: template.title, - value: { - url: isUrlTemplate(template) ? Optional.from(template.url) : Optional.none(), - content: !isUrlTemplate(template) ? Optional.from(template.content) : Optional.none(), - description: template.description - } - }; - })); - }; - const createSelectBoxItems = templates => map(templates, t => ({ - text: t.text, - value: t.text - })); - const findTemplate = (templates, templateTitle) => find(templates, t => t.text === templateTitle); - const loadFailedAlert = api => { - editor.windowManager.alert('Could not load the specified template.', () => api.focus('template')); - }; - const getTemplateContent = t => t.value.url.fold(() => Promise.resolve(t.value.content.getOr('')), url => fetch(url).then(res => res.ok ? res.text() : Promise.reject())); - const onChange = (templates, updateDialog) => (api, change) => { - if (change.name === 'template') { - const newTemplateTitle = api.getData().template; - findTemplate(templates, newTemplateTitle).each(t => { - api.block('Loading...'); - getTemplateContent(t).then(previewHtml => { - updateDialog(api, t, previewHtml); - }).catch(() => { - updateDialog(api, t, ''); - api.setEnabled('save', false); - loadFailedAlert(api); - }); - }); - } - }; - const onSubmit = templates => api => { - const data = api.getData(); - findTemplate(templates, data.template).each(t => { - getTemplateContent(t).then(previewHtml => { - editor.execCommand('mceInsertTemplate', false, previewHtml); - api.close(); - }).catch(() => { - api.setEnabled('save', false); - loadFailedAlert(api); - }); - }); - }; - const openDialog = templates => { - const selectBoxItems = createSelectBoxItems(templates); - const buildDialogSpec = (bodyItems, initialData) => ({ - title: 'Insert Template', - size: 'large', - body: { - type: 'panel', - items: bodyItems - }, - initialData, - buttons: [ - { - type: 'cancel', - name: 'cancel', - text: 'Cancel' - }, - { - type: 'submit', - name: 'save', - text: 'Save', - primary: true - } - ], - onSubmit: onSubmit(templates), - onChange: onChange(templates, updateDialog) - }); - const updateDialog = (dialogApi, template, previewHtml) => { - const content = getPreviewContent(editor, previewHtml); - const bodyItems = [ - { - type: 'listbox', - name: 'template', - label: 'Templates', - items: selectBoxItems - }, - { - type: 'htmlpanel', - html: `

${ htmlEscape(template.value.description) }

` - }, - { - label: 'Preview', - type: 'iframe', - name: 'preview', - sandboxed: false, - transparent: false - } - ]; - const initialData = { - template: template.text, - preview: content - }; - dialogApi.unblock(); - dialogApi.redial(buildDialogSpec(bodyItems, initialData)); - dialogApi.focus('template'); - }; - const dialogApi = editor.windowManager.open(buildDialogSpec([], { - template: '', - preview: '' - })); - dialogApi.block('Loading...'); - getTemplateContent(templates[0]).then(previewHtml => { - updateDialog(dialogApi, templates[0], previewHtml); - }).catch(() => { - updateDialog(dialogApi, templates[0], ''); - dialogApi.setEnabled('save', false); - loadFailedAlert(dialogApi); - }); - }; - const optTemplates = createTemplates(); - optTemplates.each(openDialog); - }; - - const showDialog = editor => templates => { - open(editor, templates); - }; - const register$1 = editor => { - editor.addCommand('mceInsertTemplate', curry(insertTemplate, editor)); - editor.addCommand('mceTemplate', createTemplateList(editor, showDialog(editor))); - }; - - const setup = editor => { - editor.on('PreProcess', o => { - const dom = editor.dom, dateFormat = getMdateFormat(editor); - global$2.each(dom.select('div', o.node), e => { - if (dom.hasClass(e, 'mceTmpl')) { - global$2.each(dom.select('*', e), e => { - if (hasAnyClasses(dom, e, getModificationDateClasses(editor))) { - e.innerHTML = getDateTime(editor, dateFormat); - } - }); - replaceVals(editor, e); - } - }); - }); - }; - - const onSetupEditable = editor => api => { - const nodeChanged = () => { - api.setEnabled(editor.selection.isEditable()); - }; - editor.on('NodeChange', nodeChanged); - nodeChanged(); - return () => { - editor.off('NodeChange', nodeChanged); - }; - }; - const register = editor => { - const onAction = () => editor.execCommand('mceTemplate'); - editor.ui.registry.addButton('template', { - icon: 'template', - tooltip: 'Insert template', - onSetup: onSetupEditable(editor), - onAction - }); - editor.ui.registry.addMenuItem('template', { - icon: 'template', - text: 'Insert template...', - onSetup: onSetupEditable(editor), - onAction - }); - }; - - var Plugin = () => { - global$3.add('template', editor => { - register$2(editor); - register(editor); - register$1(editor); - setup(editor); - }); - }; - - Plugin(); - -})(); diff --git a/public/vendor/tinymce/plugins/template/plugin.min.js b/public/vendor/tinymce/plugins/template/plugin.min.js deleted file mode 100644 index 95692e4f..00000000 --- a/public/vendor/tinymce/plugins/template/plugin.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TinyMCE version 6.7.0 (2023-08-30) - */ -!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(a=n=e,(r=String).prototype.isPrototypeOf(a)||(null===(s=n.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var a,n,r,s})(t)===e,a=t("string"),n=t("object"),r=t("array"),s=("function",e=>"function"==typeof e);const l=(!1,()=>false);var o=tinymce.util.Tools.resolve("tinymce.util.Tools");const c=e=>t=>t.options.get(e),i=c("template_cdate_classes"),u=c("template_mdate_classes"),m=c("template_selected_content_classes"),p=c("template_preview_replace_values"),d=c("template_replace_values"),h=c("templates"),g=c("template_cdate_format"),v=c("template_mdate_format"),f=c("content_style"),y=c("content_css_cors"),b=c("body_class"),_=(e,t)=>{if((e=""+e).length{const n="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),r="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),s="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),l="January February March April May June July August September October November December".split(" ");return(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+a.getFullYear())).replace("%y",""+a.getYear())).replace("%m",_(a.getMonth()+1,2))).replace("%d",_(a.getDate(),2))).replace("%H",""+_(a.getHours(),2))).replace("%M",""+_(a.getMinutes(),2))).replace("%S",""+_(a.getSeconds(),2))).replace("%I",""+((a.getHours()+11)%12+1))).replace("%p",a.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(l[a.getMonth()]))).replace("%b",""+e.translate(s[a.getMonth()]))).replace("%A",""+e.translate(r[a.getDay()]))).replace("%a",""+e.translate(n[a.getDay()]))).replace("%%","%")};class T{constructor(e,t){this.tag=e,this.value=t}static some(e){return new T(!0,e)}static none(){return T.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?T.some(e(this.value)):T.none()}bind(e){return this.tag?e(this.value):T.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:T.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return null==e?T.none():T.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}T.singletonNone=new T(!1);const S=Object.hasOwnProperty;var x=tinymce.util.Tools.resolve("tinymce.html.Serializer");const C={'"':""","<":"<",">":">","&":"&","'":"'"},w=e=>e.replace(/["'<>&]/g,(e=>{return(t=C,a=e,((e,t)=>S.call(e,t))(t,a)?T.from(t[a]):T.none()).getOr(e);var t,a})),O=(e,t,a)=>((a,n)=>{for(let n=0,s=a.length;nx({validate:!0},e.schema).serialize(e.parser.parse(t,{insert:!0})),D=(e,t)=>(o.each(t,((t,a)=>{s(t)&&(t=t(a)),e=e.replace(new RegExp("\\{\\$"+a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\}","g"),t)})),e),N=(e,t)=>{const a=e.dom,n=d(e);o.each(a.select("*",t),(e=>{o.each(n,((t,n)=>{a.hasClass(e,n)&&s(t)&&t(e)}))}))},I=(e,t,a)=>{const n=e.dom,r=e.selection.getContent();a=D(a,d(e));let s=n.create("div",{},A(e,a));const l=n.select(".mceTmpl",s);l&&l.length>0&&(s=n.create("div"),s.appendChild(l[0].cloneNode(!0))),o.each(n.select("*",s),(t=>{O(n,t,i(e))&&(t.innerHTML=M(e,g(e))),O(n,t,u(e))&&(t.innerHTML=M(e,v(e))),O(n,t,m(e))&&(t.innerHTML=r)})),N(e,s),e.execCommand("mceInsertContent",!1,s.innerHTML),e.addVisual()};var E=tinymce.util.Tools.resolve("tinymce.Env");const k=(e,t)=>{const a=(e,t)=>((e,t,a)=>{for(let n=0,r=e.length;ne.text===t),l),n=t=>{e.windowManager.alert("Could not load the specified template.",(()=>t.focus("template")))},r=e=>e.value.url.fold((()=>Promise.resolve(e.value.content.getOr(""))),(e=>fetch(e).then((e=>e.ok?e.text():Promise.reject())))),s=(e,t)=>(s,l)=>{if("template"===l.name){const l=s.getData().template;a(e,l).each((e=>{s.block("Loading..."),r(e).then((a=>{t(s,e,a)})).catch((()=>{t(s,e,""),s.setEnabled("save",!1),n(s)}))}))}},c=t=>s=>{const l=s.getData();a(t,l.template).each((t=>{r(t).then((t=>{e.execCommand("mceInsertTemplate",!1,t),s.close()})).catch((()=>{s.setEnabled("save",!1),n(s)}))}))};(()=>{if(!t||0===t.length){const t=e.translate("No templates defined.");return e.notificationManager.open({text:t,type:"info"}),T.none()}return T.from(o.map(t,((e,t)=>{const a=e=>void 0!==e.url;return{selected:0===t,text:e.title,value:{url:a(e)?T.from(e.url):T.none(),content:a(e)?T.none():T.from(e.content),description:e.description}}})))})().each((t=>{const a=(e=>((e,t)=>{const a=e.length,n=new Array(a);for(let t=0;t({title:"Insert Template",size:"large",body:{type:"panel",items:e},initialData:a,buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],onSubmit:c(t),onChange:s(t,i)}),i=(t,n,r)=>{const s=((e,t)=>{var a;let n=A(e,t);if(-1===t.indexOf("")){let t="";const r=null!==(a=f(e))&&void 0!==a?a:"",s=y(e)?' crossorigin="anonymous"':"";o.each(e.contentCSS,(a=>{t+='"})),r&&(t+='");const l=b(e),c=e.dom.encode,i=' + @endif +@endpush diff --git a/resources/views/articles/fields.blade.php b/resources/views/articles/fields.blade.php index c30e7734..ff1b2d50 100644 --- a/resources/views/articles/fields.blade.php +++ b/resources/views/articles/fields.blade.php @@ -1,4 +1,5 @@
+
@@ -22,7 +23,14 @@
{!! Html::label('Kategori', 'category_id') !!} - {!! Html::select('category_id', $categories)->class('form-control select2')->required()->value(old('category_id', $article->category_id ?? null)) !!} + @if(count($categories) == 0) + {!! Html::select('category_id', $categories)->class('form-control select2')->required()->value(old('category_id', $article->category_id ?? null))->attribute('disabled', 'disabled') !!} + + Kategori artikel belum tersedia. Buat kategori artikel sekarang. + + @else + {!! Html::select('category_id', $categories)->class('form-control select2')->required()->value(old('category_id', $article->category_id ?? null)) !!} + @endif
diff --git a/tests/Feature/ArticleControllerCmsTest.php b/tests/Feature/ArticleControllerCmsTest.php index b0ea536c..e7243f7e 100644 --- a/tests/Feature/ArticleControllerCmsTest.php +++ b/tests/Feature/ArticleControllerCmsTest.php @@ -111,4 +111,54 @@ public function test_destroy_ajax_menghapus_artikel_dengan_json() $response->assertJson(['success' => true]); $this->assertSoftDeleted('articles', ['id' => $article->id]); } + + // ========================================================= + // Test Pengecekan Kategori + // ========================================================= + + /** + * Halaman create harus mengirimkan variabel $categories ke view. + */ + public function test_create_mengirim_variabel_categories_ke_view() + { + Category::factory()->create(); // pastikan ada minimal 1 kategori + + $response = $this->get(route('articles.create')); + + $response->assertStatus(200) + ->assertViewHas('categories'); + } + + /** + * Ketika tidak ada kategori, halaman create harus menampilkan + * script peringatan SweetAlert agar pengguna diarahkan membuat kategori. + */ + public function test_create_menampilkan_peringatan_ketika_kategori_kosong() + { + // Hapus semua kategori agar kondisi kosong tercapai + Category::query()->forceDelete(); + + $response = $this->get(route('articles.create')); + + $response->assertStatus(200) + ->assertSee('Kategori Belum Tersedia'); + } + + /** + * Store harus gagal validasi jika category_id tidak dikirim. + */ + public function test_store_gagal_validasi_tanpa_category_id() + { + $response = $this->post(route('articles.store'), [ + 'title' => 'Artikel Tanpa Kategori', + 'slug' => 'artikel-tanpa-kategori', + 'content' => 'Isi konten artikel.', + 'published_at' => now()->format('d/m/Y'), + 'state' => 1, + // 'category_id' sengaja tidak dikirim + ]); + + $response->assertSessionHasErrors('category_id'); + $this->assertDatabaseMissing('articles', ['title' => 'Artikel Tanpa Kategori']); + } } From 0ba4167641d71e77f0ebf4618b3e5d80a4a725d0 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Fri, 8 May 2026 16:00:27 +0700 Subject: [PATCH 13/19] Feat/detail statistik jaminan sosial (#1030) * feat: Detail statistik Jaminan Sosial * simpan dulu * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- .../DataPresisiJaminanSosialController.php | 25 + .../StatistikJaminanSosialController.php | 6 +- .../DetailDataJaminanSosialRequest.php | 23 + catatan_rilis.md | 7 +- .../jaminan_sosial/detail_data.blade.php | 143 ++++ .../statistik/jaminan-sosial.blade.php | 755 +++++++++--------- routes/web.php | 4 + ...DataPresisiJaminanSosialControllerTest.php | 72 ++ 8 files changed, 649 insertions(+), 386 deletions(-) create mode 100644 app/Http/Controllers/DataPresisiJaminanSosialController.php create mode 100644 app/Http/Requests/DetailDataJaminanSosialRequest.php create mode 100644 resources/views/data_pokok/data_presisi/jaminan_sosial/detail_data.blade.php create mode 100644 tests/Feature/DataPresisiJaminanSosialControllerTest.php diff --git a/app/Http/Controllers/DataPresisiJaminanSosialController.php b/app/Http/Controllers/DataPresisiJaminanSosialController.php new file mode 100644 index 00000000..c042c817 --- /dev/null +++ b/app/Http/Controllers/DataPresisiJaminanSosialController.php @@ -0,0 +1,25 @@ +filled('judul') ? htmlspecialchars(strip_tags($request->input('judul'))) : ''; + $colomn = ''; + + $filter = $request->input('filter'); + if (isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] !== '' && $filter['nilai'] !== '') { + $colomn = $filter['tipe'] . ':' . $filter['nilai']; + } + + return view('data_pokok.data_presisi.jaminan_sosial.detail_data', [ + 'title' => $title, + 'colomn' => $colomn, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/StatistikJaminanSosialController.php b/app/Http/Controllers/StatistikJaminanSosialController.php index 02534e85..877ac224 100644 --- a/app/Http/Controllers/StatistikJaminanSosialController.php +++ b/app/Http/Controllers/StatistikJaminanSosialController.php @@ -2,12 +2,14 @@ namespace App\Http\Controllers; +use Illuminate\View\View; + class StatistikJaminanSosialController extends Controller { - public function index() + public function index(): View { return view('presisi.statistik.jaminan-sosial', [ - 'detailLink' => url(''), + 'detailLink' => url('data-presisi/jaminan-sosial/detail_data'), 'judul' => 'Jaminan Sosial' ]); } diff --git a/app/Http/Requests/DetailDataJaminanSosialRequest.php b/app/Http/Requests/DetailDataJaminanSosialRequest.php new file mode 100644 index 00000000..860223e6 --- /dev/null +++ b/app/Http/Requests/DetailDataJaminanSosialRequest.php @@ -0,0 +1,23 @@ + 'nullable|string', + 'filter' => 'nullable|array', + 'filter.tipe' => 'nullable|string', + 'filter.nilai' => 'nullable|string', + ]; + } +} \ No newline at end of file diff --git a/catatan_rilis.md b/catatan_rilis.md index 4105748d..cfc64a97 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -8,9 +8,10 @@ Di rilis ini, versi 2605.0.1 berisi penambahan dan perbaikan yang diminta penggu 4. [#1002](https://github.com/OpenSID/OpenKab/issues/1002) Buat Halaman Detail Pendidikan di Statistik Presisi 5. [#1001](https://github.com/OpenSID/OpenKab/issues/1001) Buat Halaman Detail Ketenagakerjaan di Statistik Presisi 6. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Keagamaan di Statistik Presisi -7. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor -8. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid -9. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel +7. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Jaminan Sosial di Statistik Presisi +8. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor +9. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid +10. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel diff --git a/resources/views/data_pokok/data_presisi/jaminan_sosial/detail_data.blade.php b/resources/views/data_pokok/data_presisi/jaminan_sosial/detail_data.blade.php new file mode 100644 index 00000000..c74310b3 --- /dev/null +++ b/resources/views/data_pokok/data_presisi/jaminan_sosial/detail_data.blade.php @@ -0,0 +1,143 @@ +@extends('layouts.index') + +@section('title', $title) + +@section('content_header') +

{{ $title }}

+@stop + +@section('content') +@include('partials.breadcrumbs') + +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + +
NONIKNOMOR KKNAMAJENIS BANTUAN SOSIAL YANG PERNAH DITERIMAJENIS GANGGUAN MENTAL YANG DIDERITAJENIS PENANGANAN PENDERITA GANGGUAN MENTALTANGGAL PENGISIANSTATUS PENGISIAN
+
+
+
+
+
+@endsection + +@section('js') + +@endsection \ No newline at end of file diff --git a/resources/views/presisi/statistik/jaminan-sosial.blade.php b/resources/views/presisi/statistik/jaminan-sosial.blade.php index 23000126..29007785 100644 --- a/resources/views/presisi/statistik/jaminan-sosial.blade.php +++ b/resources/views/presisi/statistik/jaminan-sosial.blade.php @@ -9,366 +9,365 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ + +
+
-
- - -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 0b78f135..77d3ab16 100644 --- a/routes/web.php +++ b/routes/web.php @@ -318,6 +318,10 @@ }) ->middleware(['permission:datapresisi-ketenagakerjaan-read']); + Route::prefix('jaminan-sosial')->group(function () { + Route::get('/detail_data', [App\Http\Controllers\DataPresisiJaminanSosialController::class, 'detailData'])->name('data-pokok.data-presisi-jaminan-sosial.detail_data'); + }) + ->middleware(['permission:datapresisi-jaminan-sosial-read']); Route::prefix('aktivitas-keagamaan')->group(function () { Route::get('detail_data', [App\Http\Controllers\DataPresisiAktivitasKeagamaanController::class, 'detailData'])->name('data-pokok.data-presisi-aktivitas-keagamaan.detail_data'); }) diff --git a/tests/Feature/DataPresisiJaminanSosialControllerTest.php b/tests/Feature/DataPresisiJaminanSosialControllerTest.php new file mode 100644 index 00000000..f8bb37e4 --- /dev/null +++ b/tests/Feature/DataPresisiJaminanSosialControllerTest.php @@ -0,0 +1,72 @@ +get('/data-presisi/jaminan-sosial/detail_data'); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.jaminan_sosial.detail_data'); + $response->assertViewHas('title', ''); + $response->assertViewHas('colomn', ''); + } + + public function testDetailDataWithJudul() + { + $judul = ''; + $response = $this->get('/data-presisi/jaminan-sosial/detail_data?judul=' . urlencode($judul)); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.jaminan_sosial.detail_data'); + $response->assertViewHas('title', htmlspecialchars(strip_tags($judul))); + $response->assertViewHas('colomn', ''); + } + + public function testDetailDataWithFilter() + { + $filter = ['tipe' => 'status', 'nilai' => 'aktif']; + $response = $this->get('/data-presisi/jaminan-sosial/detail_data?filter[tipe]=' . $filter['tipe'] . '&filter[nilai]=' . $filter['nilai']); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.jaminan_sosial.detail_data'); + $response->assertViewHas('title', ''); + $response->assertViewHas('colomn', $filter['tipe'] . ':' . $filter['nilai']); + } + + public function testDetailDataWithBothParameters() + { + $judul = 'Test Title & More'; + $filter = ['tipe' => 'jenis', 'nilai' => 'BPJS']; + $response = $this->get('/data-presisi/jaminan-sosial/detail_data?judul=' . urlencode($judul) . '&filter[tipe]=' . $filter['tipe'] . '&filter[nilai]=' . $filter['nilai']); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.jaminan_sosial.detail_data'); + $response->assertViewHas('title', htmlspecialchars(strip_tags($judul))); + $response->assertViewHas('colomn', $filter['tipe'] . ':' . $filter['nilai']); + } + + public function testDetailDataWithEmptyFilter() + { + $response = $this->get('/data-presisi/jaminan-sosial/detail_data?filter[tipe]=&filter[nilai]='); + + $response->assertStatus(200); + $response->assertViewIs('data_pokok.data_presisi.jaminan_sosial.detail_data'); + $response->assertViewHas('title', ''); + $response->assertViewHas('colomn', ''); + } +} \ No newline at end of file From 2f1e52336ec7e94ec56df20767312854669ed41e Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Fri, 8 May 2026 16:13:07 +0700 Subject: [PATCH 14/19] Merge Pull Request From feat: detail statistik kesehatan (#1034) * feat: detail kesehatan * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- .../DataPresisiKesehatanController.php | 24 + .../StatistikKesehatanController.php | 6 +- .../Requests/DetailDataKesehatanRequest.php | 24 + catatan_rilis.md | 9 +- .../kesehatan/detail_data.blade.php | 171 ++++ .../presisi/statistik/kesehatan.blade.php | 757 +++++++++--------- routes/web.php | 1 + 7 files changed, 604 insertions(+), 388 deletions(-) create mode 100644 app/Http/Requests/DetailDataKesehatanRequest.php create mode 100644 resources/views/data_pokok/data_presisi/kesehatan/detail_data.blade.php diff --git a/app/Http/Controllers/DataPresisiKesehatanController.php b/app/Http/Controllers/DataPresisiKesehatanController.php index b3f2b9a1..575bc6aa 100644 --- a/app/Http/Controllers/DataPresisiKesehatanController.php +++ b/app/Http/Controllers/DataPresisiKesehatanController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use App\Http\Requests\DetailDataKesehatanRequest; use Illuminate\Http\Request; +use Illuminate\View\View; class DataPresisiKesehatanController extends Controller { @@ -24,4 +26,26 @@ public function cetak(Request $request) { return view('data_pokok.data_presisi.kesehatan.cetak', ['filter' => $request->getQueryString()]); } + + public function detailData(DetailDataKesehatanRequest $request): View + { + $title = $request->filled('judul') ? htmlspecialchars(strip_tags($request->input('judul'))) : ''; + + $colomn = ''; + + $filter = $request->input('filter'); + $tipe = $request->input('tipe'); + $nilai = $request->input('filter.nilai'); + + if (is_array($filter) && isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] !== '' && $filter['nilai'] !== '') { + $colomn = $filter['tipe'] . ':' . $filter['nilai']; + } elseif ($tipe && $nilai) { + $colomn = $tipe . ':' . $nilai; + } + + return view('data_pokok.data_presisi.kesehatan.detail_data', [ + 'title' => $title, + 'colomn' => $colomn, + ]); + } } diff --git a/app/Http/Controllers/StatistikKesehatanController.php b/app/Http/Controllers/StatistikKesehatanController.php index 28509045..70496b12 100644 --- a/app/Http/Controllers/StatistikKesehatanController.php +++ b/app/Http/Controllers/StatistikKesehatanController.php @@ -4,11 +4,11 @@ class StatistikKesehatanController extends Controller { - public function index() + public function index(): \Illuminate\View\View { return view('presisi.statistik.kesehatan', [ - 'detailLink' => url(''), - 'judul' => 'Kesehatan' + 'detailLink' => url('data-presisi/kesehatan/detail_data'), + 'judul' => 'Kesehatan' ]); } } diff --git a/app/Http/Requests/DetailDataKesehatanRequest.php b/app/Http/Requests/DetailDataKesehatanRequest.php new file mode 100644 index 00000000..9b32c303 --- /dev/null +++ b/app/Http/Requests/DetailDataKesehatanRequest.php @@ -0,0 +1,24 @@ + 'nullable|string', + 'filter' => 'nullable|array', + 'filter.tipe' => 'nullable|string', + 'filter.nilai' => 'nullable|string', + 'tipe' => 'nullable|string', + ]; + } +} \ No newline at end of file diff --git a/catatan_rilis.md b/catatan_rilis.md index cfc64a97..35bdd2e3 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -8,10 +8,11 @@ Di rilis ini, versi 2605.0.1 berisi penambahan dan perbaikan yang diminta penggu 4. [#1002](https://github.com/OpenSID/OpenKab/issues/1002) Buat Halaman Detail Pendidikan di Statistik Presisi 5. [#1001](https://github.com/OpenSID/OpenKab/issues/1001) Buat Halaman Detail Ketenagakerjaan di Statistik Presisi 6. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Keagamaan di Statistik Presisi -7. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Jaminan Sosial di Statistik Presisi -8. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor -9. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid -10. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel +7. [#999](https://github.com/OpenSID/OpenKab/issues/999) Buat Halaman Detail Jaminan Sosial di Statistik Presisi +8. [#998](https://github.com/OpenSID/OpenKab/issues/998) Buat Halaman Detail Kesehatan di Statistik Presisi +9. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor +10. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid +11. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel diff --git a/resources/views/data_pokok/data_presisi/kesehatan/detail_data.blade.php b/resources/views/data_pokok/data_presisi/kesehatan/detail_data.blade.php new file mode 100644 index 00000000..66866eac --- /dev/null +++ b/resources/views/data_pokok/data_presisi/kesehatan/detail_data.blade.php @@ -0,0 +1,171 @@ +@extends('layouts.index') + +@section('title', $title) + +@section('content_header') +

{{ $title }}

+@stop + +@section('content') +@include('partials.breadcrumbs') + +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
NONIKNOMOR KKNAMAJNS ASURANSIJNS PENGGUNAAN ALAT KONTRASEPSIJNS PENYAKIT YANG DIDERITAKUNJUNGAN KE FASKES DALAM 1 TAHUNRAWAT INAP DALAM 1 TAHUNKUNJUNGAN KE DOKTER DALAM 1 TAHUNKONDISI FISIK SEJAK LAHIRSTATUS GIZI BALITATANGGAL PENGISIANSTATUS PENGISIAN
+
+
+
+
+
+@endsection + +@section('js') + +@endsection \ No newline at end of file diff --git a/resources/views/presisi/statistik/kesehatan.blade.php b/resources/views/presisi/statistik/kesehatan.blade.php index 9ef0d604..ed06273a 100644 --- a/resources/views/presisi/statistik/kesehatan.blade.php +++ b/resources/views/presisi/statistik/kesehatan.blade.php @@ -9,366 +9,364 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ + +
+
-
- - -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 77d3ab16..ac9bb938 100644 --- a/routes/web.php +++ b/routes/web.php @@ -299,6 +299,7 @@ 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'); + Route::get('/detail_data', [App\Http\Controllers\DataPresisiKesehatanController::class, 'detailData'])->name('data-pokok.data-presisi.detail_data'); Route::get('cetak', [App\Http\Controllers\DataPresisiKesehatanController::class, 'cetak'])->name('data-pokok.data-presisi.cetak'); }) ->middleware(['permission:datapresisi-kesehatan-read']); From 717e3a1dd500f847653f3d2830b8eff7d757f89d Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Fri, 8 May 2026 16:19:57 +0700 Subject: [PATCH 15/19] Merge Pull Request From feat: detail statistik seni (#1036) * feat: detail statistik seni * perbaikan judul * tambahkan judul * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- .../DataPresisiSeniBudayaController.php | 46 +- .../StatistikSenibudayaController.php | 18 +- .../DetailDataPresisiSeniBudayaRequest.php | 37 + catatan_rilis.md | 7 +- .../seni_budaya/detail_data.blade.php | 146 ++++ .../presisi/statistik/senibudaya.blade.php | 756 +++++++++--------- routes/web.php | 1 + .../DataPresisiSeniBudayaControllerTest.php | 165 ++++ 8 files changed, 787 insertions(+), 389 deletions(-) create mode 100644 app/Http/Requests/DetailDataPresisiSeniBudayaRequest.php create mode 100644 resources/views/data_pokok/data_presisi/seni_budaya/detail_data.blade.php create mode 100644 tests/Feature/DataPresisiSeniBudayaControllerTest.php diff --git a/app/Http/Controllers/DataPresisiSeniBudayaController.php b/app/Http/Controllers/DataPresisiSeniBudayaController.php index 16516f38..b1e7701a 100644 --- a/app/Http/Controllers/DataPresisiSeniBudayaController.php +++ b/app/Http/Controllers/DataPresisiSeniBudayaController.php @@ -2,25 +2,65 @@ namespace App\Http\Controllers; +use App\Http\Requests\DetailDataPresisiSeniBudayaRequest; use Illuminate\Http\Request; +use Illuminate\View\View; +/** + * Controller untuk Data Presisi Seni Budaya + * + * Menangani operasi CRUD dan display untuk data seni budaya + */ class DataPresisiSeniBudayaController extends Controller { - public function index() + /** + * Menampilkan halaman utama data seni budaya + */ + public function index(): View { $title = 'Data Presisi Seni Budaya'; return view('data_pokok.data_presisi.seni_budaya.index', compact('title')); } - public function detail(Request $request) + /** + * Menampilkan detail data seni budaya berdasarkan data yang dipilih + */ + public function detail(Request $request): View { $data = json_decode($request->data); return view('data_pokok.data_presisi.seni_budaya.detail', ['data' => $data]); } - public function cetak(Request $request) + /** + * Menampilkan halaman detail data statistik seni budaya + * dengan filter berdasarkan kategori dan nilai + */ + public function detailData(DetailDataPresisiSeniBudayaRequest $request): View + { + $title = 'Seni Budaya - '.($request->filled('judul') ? htmlspecialchars(strip_tags($request->input('judul'))) : ''); + + $colomn = ''; + + $filter = $request->input('filter'); + if (isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] !== '' && $filter['nilai'] !== '') { + if ($filter['tipe'] === 'jenis_seni_yang_dikuasai') { + $filter['tipe'] = $filter['tipe'].'.sub_jenis_seni'; + } + $colomn = $filter['tipe'].':'.$filter['nilai']; + } + + return view('data_pokok.data_presisi.seni_budaya.detail_data', [ + 'title' => $title, + 'colomn' => $colomn, + ]); + } + + /** + * Menampilkan halaman cetak data seni budaya + */ + public function cetak(Request $request): View { return view('data_pokok.data_presisi.seni_budaya.cetak', ['filter' => $request->getQueryString()]); } diff --git a/app/Http/Controllers/StatistikSenibudayaController.php b/app/Http/Controllers/StatistikSenibudayaController.php index 1235024d..27fe7754 100644 --- a/app/Http/Controllers/StatistikSenibudayaController.php +++ b/app/Http/Controllers/StatistikSenibudayaController.php @@ -2,13 +2,23 @@ namespace App\Http\Controllers; +use Illuminate\View\View; + +/** + * Controller untuk Statistik Seni Budaya + * + * Menampilkan data statistik seni budaya dengan link ke detail data + */ class StatistikSenibudayaController extends Controller { - public function index() + /** + * Menampilkan halaman statistik seni budaya + */ + public function index(): View { return view('presisi.statistik.senibudaya', [ - 'detailLink' => url(''), - 'judul' => 'seni budaya' + 'detailLink' => url('data-presisi/seni-budaya/detail_data'), + 'judul' => 'seni budaya', ]); - } + } } diff --git a/app/Http/Requests/DetailDataPresisiSeniBudayaRequest.php b/app/Http/Requests/DetailDataPresisiSeniBudayaRequest.php new file mode 100644 index 00000000..0e386252 --- /dev/null +++ b/app/Http/Requests/DetailDataPresisiSeniBudayaRequest.php @@ -0,0 +1,37 @@ +|string> + */ + public function rules(): array + { + return [ + 'judul' => 'nullable|string', + 'filter' => 'nullable|array', + 'filter.tipe' => 'nullable|string', + 'filter.nilai' => 'nullable|string', + ]; + } +} diff --git a/catatan_rilis.md b/catatan_rilis.md index 35bdd2e3..2ace5bc0 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -10,9 +10,10 @@ Di rilis ini, versi 2605.0.1 berisi penambahan dan perbaikan yang diminta penggu 6. [#1000](https://github.com/OpenSID/OpenKab/issues/1000) Buat Halaman Detail Keagamaan di Statistik Presisi 7. [#999](https://github.com/OpenSID/OpenKab/issues/999) Buat Halaman Detail Jaminan Sosial di Statistik Presisi 8. [#998](https://github.com/OpenSID/OpenKab/issues/998) Buat Halaman Detail Kesehatan di Statistik Presisi -9. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor -10. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid -11. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel +9. [#1005](https://github.com/OpenSID/OpenKab/issues/1005) Buat Halaman Detail Seni di Statistik Presisi +10. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor +11. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid +12. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel diff --git a/resources/views/data_pokok/data_presisi/seni_budaya/detail_data.blade.php b/resources/views/data_pokok/data_presisi/seni_budaya/detail_data.blade.php new file mode 100644 index 00000000..e0609016 --- /dev/null +++ b/resources/views/data_pokok/data_presisi/seni_budaya/detail_data.blade.php @@ -0,0 +1,146 @@ +@extends('layouts.index') + +@section('title', $title) + +@section('content_header') +

{{ html_entity_decode($title) }}

+@stop + +@section('content') +@include('partials.breadcrumbs') + +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + +
NONIKNOMOR KKNAMAJENIS SENI YANG DIKUASAIJUMLAH PENGHASILAN DARI SENITANGGAL PENGISIANSTATUS PENGISIAN
+
+
+
+
+
+@endsection + +@section('js') + +@endsection \ No newline at end of file diff --git a/resources/views/presisi/statistik/senibudaya.blade.php b/resources/views/presisi/statistik/senibudaya.blade.php index e44dfabc..bf6c843f 100644 --- a/resources/views/presisi/statistik/senibudaya.blade.php +++ b/resources/views/presisi/statistik/senibudaya.blade.php @@ -9,365 +9,363 @@ @stop @section('content') - @include('partials.breadcrumbs') -
-
-
-
-

Statistik {{ $judul }}

-
- -
-
-
- +@include('partials.breadcrumbs') +
+
+
+
+

Statistik {{ $judul }}

+
+
+
+ +
-
-
-
-
-

+
+
+
+
+
+

+
+
+ +
+
-
- -
- -
-
- -
-
- -
+
+ +
+
+
-
-
-
-
-
- -
-
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
+
+
-
- - - - - - - - - -
NoNilaiJumlah
-
+
+ + + + + + + + + +
NoNilaiJumlah
+
@endsection @section('js') - - @include('statistik.chart') - +@include('statistik.chart') + + }); + @endsection @push('css') - + @endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index ac9bb938..c5fe9747 100644 --- a/routes/web.php +++ b/routes/web.php @@ -307,6 +307,7 @@ Route::prefix('seni-budaya')->group(function () { Route::get('/', [App\Http\Controllers\DataPresisiSeniBudayaController::class, 'index'])->name('data-pokok.data-presisi-seni-budaya.index'); Route::get('/detail', [App\Http\Controllers\DataPresisiSeniBudayaController::class, 'detail'])->name('data-pokok.data-presisi-seni-budaya.detail'); + Route::get('/detail_data', [App\Http\Controllers\DataPresisiSeniBudayaController::class, 'detailData'])->name('data-pokok.data-presisi-seni-budaya.detail_data'); Route::get('cetak', [App\Http\Controllers\DataPresisiSeniBudayaController::class, 'cetak'])->name('data-pokok.data-presisi-seni-budaya.cetak'); }) ->middleware(['permission:datapresisi-seni-budaya-read']); diff --git a/tests/Feature/DataPresisiSeniBudayaControllerTest.php b/tests/Feature/DataPresisiSeniBudayaControllerTest.php new file mode 100644 index 00000000..e48507a5 --- /dev/null +++ b/tests/Feature/DataPresisiSeniBudayaControllerTest.php @@ -0,0 +1,165 @@ +controller = new DataPresisiSeniBudayaController; + } + + /** + * Helper untuk membuat request dengan data yang diberikan + */ + private function createRequest(array $data = []): DetailDataPresisiSeniBudayaRequest + { + $request = app()->make(DetailDataPresisiSeniBudayaRequest::class); + $request->merge($data); + + return $request; + } + + /** + * Test halaman utama data seni budaya + */ + public function test_index_returns_correct_view(): void + { + $response = $this->controller->index(); + + $this->assertInstanceOf(View::class, $response); + $this->assertEquals('data_pokok.data_presisi.seni_budaya.index', $response->name()); + + $data = $response->getData(); + $this->assertArrayHasKey('title', $data); + $this->assertEquals('Data Presisi Seni Budaya', $data['title']); + } + + /** + * Test detail data tanpa filter mengembalikan view yang benar + */ + public function test_detail_data_without_filter_returns_correct_view(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => '', + ])); + + $this->assertInstanceOf(View::class, $response); + $this->assertEquals('data_pokok.data_presisi.seni_budaya.detail_data', $response->name()); + + $data = $response->getData(); + $this->assertArrayHasKey('title', $data); + $this->assertArrayHasKey('colomn', $data); + $this->assertEquals('Seni Budaya - ', $data['title']); + $this->assertEquals('', $data['colomn']); + } + + /** + * Test detail data dengan filter mengembalikan view yang benar + */ + public function test_detail_data_with_filter_returns_correct_view(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + 'nilai' => 'seni', + ], + ])); + + $this->assertInstanceOf(View::class, $response); + + $data = $response->getData(); + $this->assertEquals('Seni Budaya - Test Judul', $data['title']); + $this->assertEquals('kategori:seni', $data['colomn']); + } + + /** + * Test detail data dengan filter khusus untuk jenis seni yang dikuasai + * harus menambahkan suffix .sub_jenis_seni + */ + public function test_detail_data_with_jenis_seni_filter_adds_sub_jenis_seni(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'jenis_seni_yang_dikuasai', + 'nilai' => 'batik', + ], + ])); + + $data = $response->getData(); + $this->assertEquals('jenis_seni_yang_dikuasai.sub_jenis_seni:batik', $data['colomn']); + } + + /** + * Test detail data dengan filter parsial mengembalikan colomn kosong + */ + public function test_detail_data_with_partial_filter_returns_empty_colomn(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + ], + ])); + + $data = $response->getData(); + $this->assertEquals('', $data['colomn']); + } + + /** + * Test detail data dengan filter nilai kosong mengembalikan colomn kosong + */ + public function test_detail_data_with_empty_nilai_filter_returns_empty_colomn(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test Judul', + 'filter' => [ + 'tipe' => 'kategori', + 'nilai' => '', + ], + ])); + + $data = $response->getData(); + $this->assertEquals('', $data['colomn']); + } + + /** + * Test judul disanitasi untuk mencegah XSS + */ + public function test_detail_data_title_is_sanitized_against_xss(): void + { + $response = $this->controller->detailData($this->createRequest([ + 'judul' => 'Test', + 'filter' => [], + ])); + + $data = $response->getData(); + $this->assertStringNotContainsString(' diff --git a/routes/web.php b/routes/web.php index c5fe9747..e420dd6b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name('presisi.keluarga'); Route::get('/module/kesehatan/{id}', [PresisiController::class, 'kesehatan'])->name('presisi.kesehatan'); Route::get('/statistik-kesehatan', [PresisiController::class, 'kesehatan'])->name('presisi.kesehatan'); + +// Image proxy route with rate limiting +Route::middleware('throttle:30,1')->get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy'); diff --git a/tests/Feature/Http/Controllers/ImageProxyControllerTest.php b/tests/Feature/Http/Controllers/ImageProxyControllerTest.php new file mode 100644 index 00000000..a4b7c552 --- /dev/null +++ b/tests/Feature/Http/Controllers/ImageProxyControllerTest.php @@ -0,0 +1,135 @@ +get('/image-proxy?url=invalid-url'); + + $response->assertStatus(400); + } + + /** + * Test proxy with empty URL. + */ + public function test_proxy_with_empty_url() + { + $response = $this->get('/image-proxy'); + + $response->assertStatus(400); + } + + /** + * Test proxy with valid URL but non-image content type. + */ + public function test_proxy_with_non_image_content() + { + $url = 'https://example.com/image.jpg'; + + Http::fake([ + $url => Http::response('not an image', 200, ['Content-Type' => 'text/html']), + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(400); + } + + /** + * Test proxy with successful image fetch. + */ + public function test_proxy_with_successful_image_fetch() + { + $url = 'https://example.com/image.jpg'; + $imageContent = 'fake image data'; + $contentType = 'image/jpeg'; + + Http::fake([ + $url => Http::response($imageContent, 200, ['Content-Type' => $contentType]), + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(200) + ->assertHeader('Content-Type', $contentType); + + $this->assertEquals($imageContent, $response->getContent()); + } + + /** + * Test proxy serves from cache. + */ + public function test_proxy_serves_from_cache() + { + $url = 'https://example.com/image.jpg'; + $imageContent = 'cached image data'; + $contentType = 'image/png'; + $cacheKey = 'image_proxy_' . md5($url); + + // Manually set cache + Cache::put($cacheKey, ['content' => $imageContent, 'content_type' => $contentType], 3600); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(200) + ->assertHeader('Content-Type', $contentType); + + $this->assertEquals($imageContent, $response->getContent()); + } + + /** + * Test proxy with failed external request. + */ + public function test_proxy_with_failed_external_request() + { + $url = 'https://example.com/image.jpg'; + + Http::fake([ + $url => Http::response(null, 404), + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(404); + } + + /** + * Test proxy with exception during fetch. + */ + public function test_proxy_with_exception_during_fetch() + { + $url = 'https://example.com/image.jpg'; + + Http::fake([ + $url => function () { + throw new \Exception('Network error'); + }, + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(404); + } +} \ No newline at end of file From 5b7a59cefe837457f203e6beb67206ae19075bfb Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 20 May 2026 10:21:10 +0700 Subject: [PATCH 17/19] Merge Pull Request From fix: perbaikan tampilan website ketika slider belum diisi (#1040) * fix: perbaikan tampilan website ketika slider belum diisi * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- catatan_rilis.md | 1 + public/web/css/openkab.css | 18 +++++++++++++++++- resources/views/web/partials/slider.blade.php | 8 +++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/catatan_rilis.md b/catatan_rilis.md index bf532140..50deefef 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -22,6 +22,7 @@ Di rilis ini, versi 2606.0.0 berisi penambahan dan perbaikan yang diminta penggu 2. [#1026](https://github.com/OpenSID/OpenKab/issues/1026) Perbaikan fungsi insert media dan gambar pada tinymce artikel 3. [#1032](https://github.com/OpenSID/OpenKab/issues/1032) Perbaikan Tombol enter refresh halaman di kategori artikel opensid 4. [#1037](https://github.com/OpenSID/OpenKab/issues/1037) Perbaikan Gambar desa aktif pada halaman website openkab masih statis +5. [#1039](https://github.com/OpenSID/OpenKab/issues/1039) Perbaikan Tampilan halaman web ketika belum ditambahkan gambar slider tidak tampil dengan baik #### Perubahan Teknis diff --git a/public/web/css/openkab.css b/public/web/css/openkab.css index 31a928c7..21bac905 100644 --- a/public/web/css/openkab.css +++ b/public/web/css/openkab.css @@ -196,7 +196,23 @@ a { } @media (max-width: 768px) { - .header-carousel .owl-nav { +.header-carousel { + min-height: 350px; +} + +.header-carousel .owl-carousel-item { + min-height: 350px; + display: flex; + align-items: center; + justify-content: center; +} + +.header-carousel .owl-carousel-item img { + max-height: 350px; + object-fit: contain; +} + +.header-carousel .owl-nav { left: 25px; } } diff --git a/resources/views/web/partials/slider.blade.php b/resources/views/web/partials/slider.blade.php index 036b9548..6e409560 100644 --- a/resources/views/web/partials/slider.blade.php +++ b/resources/views/web/partials/slider.blade.php @@ -1,10 +1,16 @@
From e3c1254bc062b4663bd460ceeb86442c4c0bbb82 Mon Sep 17 00:00:00 2001 From: ahmad afandi Date: Wed, 20 May 2026 10:32:21 +0700 Subject: [PATCH 18/19] Merge Pull Request From feat: datatable debounce (#1042) * feat: datatable debounce * [ci skip] memutahirkan catatan rilis --------- Co-authored-by: Ahmad Affandi --- catatan_rilis.md | 1 + resources/views/layouts/index.blade.php | 41 +++++++++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/catatan_rilis.md b/catatan_rilis.md index 50deefef..4b69b33c 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -14,6 +14,7 @@ Di rilis ini, versi 2606.0.0 berisi penambahan dan perbaikan yang diminta penggu 10. [#1021](https://github.com/OpenSID/OpenKab/issues/1021) Ubah field isi artikel menjadi rich editor 11. [#1025](https://github.com/OpenSID/OpenKab/issues/1025) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel opensid 12. [#1031](https://github.com/OpenSID/OpenKab/issues/1031) Arahkan/Infokan pembuatan kategori artikel ketika kategori kosong saat membuat artikel di pengaturan web -> artikel +13. [#1041](https://github.com/OpenSID/OpenKab/issues/1041) Tambahkan fungsi global untuk debounce search datatable diff --git a/resources/views/layouts/index.blade.php b/resources/views/layouts/index.blade.php index b697cd76..b42a9e1d 100644 --- a/resources/views/layouts/index.blade.php +++ b/resources/views/layouts/index.blade.php @@ -24,20 +24,20 @@ function apiProxyGet(endpoint, params, callback) { $.ajax({ url: url, - type: 'GET', + type: 'GET', success: function(response) { if (callback) callback(response); }, error: function(response) { if (callback) callback({ - data: [], + data: [], meta: { pagination: { total: 0 } } }); - + Swal.fire( 'Error!', response.responseJSON.error || 'Gagal mengambil data dari API', @@ -46,7 +46,7 @@ function apiProxyGet(endpoint, params, callback) { } }); } - document.addEventListener("DOMContentLoaded", function(event) { + document.addEventListener("DOMContentLoaded", function(event) { $.ajax({ type: "get", url: "/api/v1/identitas", @@ -69,7 +69,36 @@ function apiProxyGet(endpoint, params, callback) { $.extend($.fn.dataTable.defaults, { language: { url: "{{ asset('vendor/datatable/id.json') }}" - } + }, + searchDelay: 500, + }); + + $(document).on('init.dt', function(e, settings) { + if (e.namespace !== 'dt') return; + + var table = new $.fn.dataTable.Api(settings); + var searchDelay = table.init().searchDelay || 500; + var searchInput = $('div.dataTables_filter input', table.table().container()); + var debounceTimer = null; + var previousSearch = null; + + searchInput.off('keyup.DT input.DT search.DT keydown.DT'); + + searchInput.on('keyup input', function() { + var currentValue = this.value; + + if (previousSearch === currentValue) return; + + previousSearch = currentValue; + + clearTimeout(debounceTimer); + + debounceTimer = setTimeout(function() { + if (table.search() !== currentValue) { + table.search(currentValue).draw(); + } + }, searchDelay); + }); }); @@ -121,4 +150,4 @@ function filter_open() { }); }) -@endpush \ No newline at end of file +@endpush From 5d8acf320a102d93ad219b3776bd385b0772c42c Mon Sep 17 00:00:00 2001 From: habibie11 Date: Fri, 22 May 2026 10:22:26 +0700 Subject: [PATCH 19/19] rilis v2605.0.1 --- app/Helpers/general.php | 2 +- catatan_rilis.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Helpers/general.php b/app/Helpers/general.php index d4493d05..bf0d30f5 100644 --- a/app/Helpers/general.php +++ b/app/Helpers/general.php @@ -32,7 +32,7 @@ */ function openkab_versi() { - return 'v2604.0.2'; + return 'v2605.0.1'; } } diff --git a/catatan_rilis.md b/catatan_rilis.md index 4b69b33c..62ce8c61 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -1,4 +1,4 @@ -Di rilis ini, versi 2606.0.0 berisi penambahan dan perbaikan yang diminta pengguna. +Di rilis ini, versi 2605.0.1 berisi penambahan dan perbaikan yang diminta pengguna. #### Penambahan Fitur