feat: Plan 2 — Form Edit Rumah Ibadah & Keluarga Miskin + Ownership
- Add user_id FK (nullable) to rumah_ibadah for ownership tracking - IbadahController: set user_id on store; enforce ownership on update/destroy - Add edit modals to map and data views (ibadah + miskin) - Popup and table row Edit buttons with per-record ownership check - 7 new tests covering ownership and full-field updates - Fix ExampleTest for auth-protected routes - Fix UserFactory missing email_verified_at column - Mark Plan 2 complete in missing_features.md; update README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Dokumentasi Fungsional: WebGIS Miskin & Ibadah
|
# Dokumentasi Fungsional: WebGIS Pemetaan Kemiskinan
|
||||||
|
|
||||||
Proyek ini adalah sistem informasi geografis (WebGIS) berbasis web yang dirancang untuk memetakan sebaran rumah ibadah dan keluarga kurang mampu (miskin). Sistem ini memungkinkan pengguna untuk melakukan analisis spasial berdasarkan radius untuk mengetahui dampak atau jangkauan layanan rumah ibadah terhadap komunitas di sekitarnya.
|
Proyek ini adalah sistem informasi geografis (WebGIS) berbasis web yang dirancang untuk memetakan sebaran rumah ibadah dan keluarga kurang mampu (miskin). Sistem ini memungkinkan pengguna untuk melakukan analisis spasial berdasarkan radius untuk mengetahui dampak atau jangkauan layanan rumah ibadah terhadap komunitas di sekitarnya.
|
||||||
|
|
||||||
@@ -20,8 +20,10 @@ Menggunakan library **Leaflet.js**, sistem menyediakan antarmuka peta yang respo
|
|||||||
### 2. Manajemen Data (CRUD)
|
### 2. Manajemen Data (CRUD)
|
||||||
Sistem mendukung pengelolaan data secara penuh:
|
Sistem mendukung pengelolaan data secara penuh:
|
||||||
- **Tambah Data via Peta**: Klik pada lokasi di peta untuk memunculkan formulir input data baru.
|
- **Tambah Data via Peta**: Klik pada lokasi di peta untuk memunculkan formulir input data baru.
|
||||||
|
- **Edit Data via Form**: Klik tombol "Edit" pada popup marker di peta atau pada baris di tabel data untuk membuka formulir edit. Mendukung pembaruan semua field teks untuk Rumah Ibadah (nama, jenis, pengurus, telepon, jemaah, radius, keterangan) dan Keluarga Miskin (nama KK, jumlah jiwa, pekerjaan, pendapatan, kondisi, keterangan).
|
||||||
- **Update Lokasi (Drag & Drop)**: Marker dapat digeser (drag) untuk memperbarui koordinat lokasi di database secara real-time.
|
- **Update Lokasi (Drag & Drop)**: Marker dapat digeser (drag) untuk memperbarui koordinat lokasi di database secara real-time.
|
||||||
- **Hapus Data**: Menghapus data melalui popup pada marker atau melalui tabel data.
|
- **Hapus Data**: Menghapus data melalui popup pada marker atau melalui tabel data.
|
||||||
|
- **Ownership Rumah Ibadah**: Pengurus Rumah Ibadah hanya dapat mengubah atau menghapus data rumah ibadah yang mereka tambahkan sendiri. Administrator dapat mengubah semua data.
|
||||||
- **Data Tabular**: Halaman khusus yang menampilkan seluruh data dalam bentuk tabel dengan statistik ringkasan.
|
- **Data Tabular**: Halaman khusus yang menampilkan seluruh data dalam bentuk tabel dengan statistik ringkasan.
|
||||||
|
|
||||||
### 3. Analisis Spasial Radius
|
### 3. Analisis Spasial Radius
|
||||||
@@ -33,7 +35,7 @@ Fitur unggulan untuk menganalisis sebaran keluarga miskin di sekitar rumah ibada
|
|||||||
|
|
||||||
## Arsitektur Teknis
|
## Arsitektur Teknis
|
||||||
|
|
||||||
Proyek ini telah dimigrasikan ke framework **Laravel 11** dengan struktur sebagai berikut:
|
Proyek ini telah dimigrasikan ke framework **Laravel 13** dengan struktur sebagai berikut:
|
||||||
|
|
||||||
- **Backend**:
|
- **Backend**:
|
||||||
- **Laravel Eloquent**: Digunakan untuk interaksi database yang aman dan efisien.
|
- **Laravel Eloquent**: Digunakan untuk interaksi database yang aman dan efisien.
|
||||||
@@ -45,6 +47,87 @@ Proyek ini telah dimigrasikan ke framework **Laravel 11** dengan struktur sebaga
|
|||||||
- **JavaScript (Vanilla)**: Menangani logika peta dan interaksi API tanpa beban framework berat.
|
- **JavaScript (Vanilla)**: Menangani logika peta dan interaksi API tanpa beban framework berat.
|
||||||
- **Database**: MySQL/MariaDB untuk penyimpanan data spasial dan atribut.
|
- **Database**: MySQL/MariaDB untuk penyimpanan data spasial dan atribut.
|
||||||
|
|
||||||
|
## Cara Menjalankan Project
|
||||||
|
|
||||||
|
Cara utama untuk menjalankan project ini adalah menggunakan **Docker Compose**. Setup Docker sudah mencakup Laravel app, Vite dev server, dan MySQL.
|
||||||
|
|
||||||
|
### Prasyarat
|
||||||
|
|
||||||
|
- Docker Desktop sudah terinstall dan berjalan.
|
||||||
|
- Port `8000`, `5173`, dan `3307` sedang tidak dipakai aplikasi lain.
|
||||||
|
|
||||||
|
### Menjalankan dengan Docker
|
||||||
|
|
||||||
|
Jalankan perintah berikut dari root project:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Container `app` akan otomatis:
|
||||||
|
|
||||||
|
- menginstall dependency Composer jika `vendor` belum ada di volume Docker,
|
||||||
|
- membuat file `.env` dari `.env.docker.example` jika `.env` belum ada,
|
||||||
|
- generate `APP_KEY` jika masih kosong,
|
||||||
|
- membersihkan config cache,
|
||||||
|
- menjalankan migration ke database MySQL.
|
||||||
|
|
||||||
|
Container `vite` akan otomatis menginstall dependency Node jika `node_modules` belum ada di volume Docker, lalu menjalankan Vite dev server.
|
||||||
|
|
||||||
|
Setelah container berjalan, buka:
|
||||||
|
|
||||||
|
- Aplikasi: `http://localhost:8000`
|
||||||
|
- Vite dev server: `http://localhost:5173`
|
||||||
|
- MySQL dari host: `127.0.0.1:3307`
|
||||||
|
- MySQL dari container Laravel: `mysql:3306`
|
||||||
|
|
||||||
|
### Konfigurasi Database
|
||||||
|
|
||||||
|
Konfigurasi database development menggunakan MySQL:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=mysql
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=webgis_miskin
|
||||||
|
DB_USERNAME=webgis
|
||||||
|
DB_PASSWORD=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Credential MySQL root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MYSQL_ROOT_PASSWORD=root
|
||||||
|
```
|
||||||
|
|
||||||
|
### Perintah yang Sering Dipakai
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs -f app
|
||||||
|
docker compose logs -f vite
|
||||||
|
docker compose exec app php artisan migrate:status
|
||||||
|
docker compose exec app php artisan migrate
|
||||||
|
docker compose exec app php artisan test
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
Untuk menghapus database MySQL Docker dan mulai dari database kosong:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menjalankan Artisan di Docker
|
||||||
|
|
||||||
|
Semua perintah Artisan dijalankan lewat service `app`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose exec app php artisan route:list
|
||||||
|
docker compose exec app php artisan migrate:fresh
|
||||||
|
docker compose exec app php artisan cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
## Daftar Halaman
|
## Daftar Halaman
|
||||||
|
|
||||||
1. **Beranda (`/`)**: Halaman landing dengan latar belakang peta animasi dan navigasi cepat.
|
1. **Beranda (`/`)**: Halaman landing dengan latar belakang peta animasi dan navigasi cepat.
|
||||||
@@ -52,4 +135,4 @@ Proyek ini telah dimigrasikan ke framework **Laravel 11** dengan struktur sebaga
|
|||||||
3. **Data Tabular (`/data`)**: Manajemen data dalam format tabel dengan ringkasan statistik total.
|
3. **Data Tabular (`/data`)**: Manajemen data dalam format tabel dengan ringkasan statistik total.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Dokumentasi ini dibuat untuk menjelaskan kapabilitas sistem WebGIS Miskin & Ibadah versi Laravel.*
|
*Dokumentasi ini dibuat untuk menjelaskan kapabilitas sistem WebGIS Pemetaan Kemiskinan versi Laravel.*
|
||||||
|
|||||||
@@ -16,12 +16,18 @@ class IbadahController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'nama' => 'required|string',
|
'nama' => 'required|string',
|
||||||
'alamat' => 'required|string',
|
'jenis' => 'nullable|string',
|
||||||
|
'alamat' => 'nullable|string',
|
||||||
'latitude' => 'required|numeric',
|
'latitude' => 'required|numeric',
|
||||||
'longitude' => 'required|numeric',
|
'longitude' => 'required|numeric',
|
||||||
'radius' => 'nullable|integer',
|
'radius' => 'nullable|integer',
|
||||||
|
'jumlah_jemaah' => 'nullable|integer',
|
||||||
|
'pengurus' => 'nullable|string',
|
||||||
|
'telepon' => 'nullable|string',
|
||||||
|
'keterangan' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$validated['user_id'] = auth()->id();
|
||||||
$ibadah = RumahIbadah::create($validated);
|
$ibadah = RumahIbadah::create($validated);
|
||||||
return response()->json(['success' => true, 'data' => $ibadah]);
|
return response()->json(['success' => true, 'data' => $ibadah]);
|
||||||
}
|
}
|
||||||
@@ -29,12 +35,22 @@ class IbadahController extends Controller
|
|||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
$ibadah = RumahIbadah::findOrFail($id);
|
$ibadah = RumahIbadah::findOrFail($id);
|
||||||
|
|
||||||
|
if (!auth()->user()->hasRole('administrator') && $ibadah->user_id !== auth()->id()) {
|
||||||
|
return response()->json(['message' => 'Tidak diizinkan mengubah data ini.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'nama' => 'required|string',
|
'nama' => 'required|string',
|
||||||
'alamat' => 'required|string',
|
'jenis' => 'nullable|string',
|
||||||
|
'alamat' => 'nullable|string',
|
||||||
'latitude' => 'required|numeric',
|
'latitude' => 'required|numeric',
|
||||||
'longitude' => 'required|numeric',
|
'longitude' => 'required|numeric',
|
||||||
'radius' => 'required|integer',
|
'radius' => 'required|integer',
|
||||||
|
'jumlah_jemaah' => 'nullable|integer',
|
||||||
|
'pengurus' => 'nullable|string',
|
||||||
|
'telepon' => 'nullable|string',
|
||||||
|
'keterangan' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$ibadah->update($validated);
|
$ibadah->update($validated);
|
||||||
@@ -44,6 +60,11 @@ class IbadahController extends Controller
|
|||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$ibadah = RumahIbadah::findOrFail($id);
|
$ibadah = RumahIbadah::findOrFail($id);
|
||||||
|
|
||||||
|
if (!auth()->user()->hasRole('administrator') && $ibadah->user_id !== auth()->id()) {
|
||||||
|
return response()->json(['message' => 'Tidak diizinkan menghapus data ini.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$ibadah->delete();
|
$ibadah->delete();
|
||||||
return response()->json(['success' => true]);
|
return response()->json(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,10 @@ class RumahIbadah extends Model
|
|||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'rumah_ibadah';
|
protected $table = 'rumah_ibadah';
|
||||||
protected $fillable = ['nama', 'alamat', 'latitude', 'longitude', 'radius'];
|
protected $fillable = ['user_id', 'nama', 'jenis', 'alamat', 'latitude', 'longitude', 'radius', 'jumlah_jemaah', 'pengurus', 'telepon', 'keterangan'];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Spatie\Permission\Traits\HasRoles;
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
use HasRoles, Notifiable;
|
use HasFactory, HasRoles, Notifiable;
|
||||||
|
|
||||||
protected $fillable = ['name', 'email', 'password'];
|
protected $fillable = ['name', 'email', 'password'];
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class UserFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'name' => fake()->name(),
|
'name' => fake()->name(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
'email_verified_at' => now(),
|
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => Str::random(10),
|
||||||
];
|
];
|
||||||
@@ -38,8 +37,6 @@ class UserFactory extends Factory
|
|||||||
*/
|
*/
|
||||||
public function unverified(): static
|
public function unverified(): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => []);
|
||||||
'email_verified_at' => null,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('rumah_ibadah', function (Blueprint $table) {
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete()->after('id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('rumah_ibadah', function (Blueprint $table) {
|
||||||
|
$table->dropForeignIfExists(['user_id']);
|
||||||
|
$table->dropColumn('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+8
-8
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Berdasarkan perbandingan dokumen SKPL (`D1041231009_RadityaIndraPutranto_SKPL_SIG.docx`) dengan implementasi aktual project.
|
> Berdasarkan perbandingan dokumen SKPL (`D1041231009_RadityaIndraPutranto_SKPL_SIG.docx`) dengan implementasi aktual project.
|
||||||
>
|
>
|
||||||
> **Terakhir diperbarui:** 2026-06-02 — setelah implementasi Auth & RBAC (Plan 1).
|
> **Terakhir diperbarui:** 2026-06-02 — setelah implementasi Form Edit & Ownership (Plan 2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
### 1.3 Gap ownership yang ditemukan saat implementasi:
|
### 1.3 Gap ownership yang ditemukan saat implementasi:
|
||||||
|
|
||||||
- [ ] **Ownership Rumah Ibadah** — `pengurus_ibadah` seharusnya hanya bisa edit/hapus data ibadah miliknya sendiri. Saat ini bisa edit/hapus **semua** ibadah. Butuh kolom `user_id` di tabel `rumah_ibadah` + policy/gate untuk enforce ownership.
|
- [x] **Ownership Rumah Ibadah** — `pengurus_ibadah` seharusnya hanya bisa edit/hapus data ibadah miliknya sendiri. Saat ini bisa edit/hapus **semua** ibadah. Butuh kolom `user_id` di tabel `rumah_ibadah` + policy/gate untuk enforce ownership.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ Data bisa ditambah dan dihapus. Untuk lokasi: drag marker (ibadah & miskin) dan
|
|||||||
|
|
||||||
| Fitur | Role terkait | Status |
|
| Fitur | Role terkait | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Form Edit Rumah Ibadah** — Edit nama, jenis, pengurus, telepon, jemaah, keterangan | I-06, A-02 | ❌ Belum ada |
|
| **Form Edit Rumah Ibadah** — Edit nama, jenis, pengurus, telepon, jemaah, keterangan | I-06, A-02 | ✅ Selesai |
|
||||||
| **Form Edit Keluarga Miskin** — Edit nama KK, jumlah jiwa, pekerjaan, pendapatan, kondisi | P-02, A-02 | ❌ Belum ada |
|
| **Form Edit Keluarga Miskin** — Edit nama KK, jumlah jiwa, pekerjaan, pendapatan, kondisi | P-02, A-02 | ✅ Selesai |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -84,12 +84,12 @@ Data bisa ditambah dan dihapus. Untuk lokasi: drag marker (ibadah & miskin) dan
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Autentikasi & RBAC | ✅ 6/6 komponen | — |
|
| Autentikasi & RBAC | ✅ 6/6 komponen | — |
|
||||||
| Role Pengguna | ✅ 4/4 role | — |
|
| Role Pengguna | ✅ 4/4 role | — |
|
||||||
| Ownership pengurus_ibadah | — | ❌ 1 item baru |
|
| Ownership pengurus_ibadah | ✅ 1/1 | — |
|
||||||
| Fitur Admin | — | ❌ 8 fitur |
|
| Fitur Admin | — | ❌ 8 fitur |
|
||||||
| Fitur Edit Data | — | ❌ 2 fitur |
|
| Fitur Edit Data | ✅ 2/2 | — |
|
||||||
| Fitur Analisis/Dashboard | — | ❌ 3 fitur |
|
| Fitur Analisis/Dashboard | — | ❌ 3 fitur |
|
||||||
| Fitur Cetak/Ekspor | — | ❌ 3 fitur |
|
| Fitur Cetak/Ekspor | — | ❌ 3 fitur |
|
||||||
| **Total sisa** | | **~17 item** |
|
| **Total sisa** | | **~14 item** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ Data bisa ditambah dan dihapus. Untuk lokasi: drag marker (ibadah & miskin) dan
|
|||||||
| Plan | Isi | Prioritas |
|
| Plan | Isi | Prioritas |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ~~Plan 1~~ | ~~Auth & RBAC~~ | ✅ Selesai |
|
| ~~Plan 1~~ | ~~Auth & RBAC~~ | ✅ Selesai |
|
||||||
| Plan 2 | Form Edit Rumah Ibadah & Keluarga Miskin + Ownership | Tinggi |
|
| ~~Plan 2~~ | ~~Form Edit Rumah Ibadah & Keluarga Miskin + Ownership~~ | ✅ Selesai |
|
||||||
| Plan 3 | Manajemen Pengguna (Admin: CRUD user, assign role) | Tinggi |
|
| Plan 3 | Manajemen Pengguna (Admin: CRUD user, assign role) | Tinggi |
|
||||||
| Plan 4 | Dashboard Statistik + Gap Analysis (Pemangku Kebijakan) | Sedang |
|
| Plan 4 | Dashboard Statistik + Gap Analysis (Pemangku Kebijakan) | Sedang |
|
||||||
| Plan 5 | Ekspor PDF + Ekspor per Radius + Cetak Laporan | Sedang |
|
| Plan 5 | Ekspor PDF + Ekspor per Radius + Cetak Laporan | Sedang |
|
||||||
|
|||||||
@@ -157,6 +157,120 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Modal: Edit Rumah Ibadah -->
|
||||||
|
<dialog id="modal_edit_ibadah" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-lg">
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
<i class="fa-solid fa-mosque mr-2 text-success"></i>Edit Rumah Ibadah
|
||||||
|
</h3>
|
||||||
|
<input type="hidden" id="edit_ib_id">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Nama *</span></label>
|
||||||
|
<input id="edit_ib_nama" type="text" class="input input-bordered w-full" placeholder="Nama rumah ibadah">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jenis</span></label>
|
||||||
|
<select id="edit_ib_jenis" class="select select-bordered w-full">
|
||||||
|
<option value="">Pilih Jenis</option>
|
||||||
|
<option value="masjid">Masjid</option>
|
||||||
|
<option value="gereja">Gereja</option>
|
||||||
|
<option value="pura">Pura</option>
|
||||||
|
<option value="klenteng">Klenteng</option>
|
||||||
|
<option value="vihara">Vihara</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Radius Analisis (m)</span></label>
|
||||||
|
<input id="edit_ib_radius" type="number" class="input input-bordered w-full" value="500" min="50" max="2000">
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Alamat</span></label>
|
||||||
|
<textarea id="edit_ib_alamat" class="textarea textarea-bordered w-full" rows="2" placeholder="Alamat lengkap"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Pengurus</span></label>
|
||||||
|
<input id="edit_ib_pengurus" type="text" class="input input-bordered w-full" placeholder="Nama pengurus">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Telepon</span></label>
|
||||||
|
<input id="edit_ib_telepon" type="text" class="input input-bordered w-full" placeholder="No. telepon">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jumlah Jemaah</span></label>
|
||||||
|
<input id="edit_ib_jemaah" type="number" class="input input-bordered w-full" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
|
||||||
|
<textarea id="edit_ib_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button onclick="document.getElementById('modal_edit_ibadah').close()" class="btn btn-ghost">Batal</button>
|
||||||
|
<button onclick="saveEditIbadah()" class="btn btn-success">
|
||||||
|
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Modal: Edit Keluarga Miskin -->
|
||||||
|
<dialog id="modal_edit_miskin" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-lg">
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
<i class="fa-solid fa-house mr-2 text-error"></i>Edit Keluarga Miskin
|
||||||
|
</h3>
|
||||||
|
<input type="hidden" id="edit_rm_id">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Nama Kepala Keluarga</span></label>
|
||||||
|
<input id="edit_rm_nama_kk" type="text" class="input input-bordered w-full" placeholder="Nama KK">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jumlah KK</span></label>
|
||||||
|
<input id="edit_rm_kk" type="number" class="input input-bordered w-full" value="1" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jumlah Jiwa</span></label>
|
||||||
|
<input id="edit_rm_jiwa" type="number" class="input input-bordered w-full" value="4" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Alamat</span></label>
|
||||||
|
<textarea id="edit_rm_alamat" class="textarea textarea-bordered w-full" rows="2" placeholder="Alamat lengkap"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Pekerjaan</span></label>
|
||||||
|
<input id="edit_rm_kerja" type="text" class="input input-bordered w-full" placeholder="Pekerjaan KK">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Pendapatan (Rp)</span></label>
|
||||||
|
<input id="edit_rm_duit" type="number" class="input input-bordered w-full" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Kondisi Rumah</span></label>
|
||||||
|
<select id="edit_rm_kondisi" class="select select-bordered w-full">
|
||||||
|
<option value="">Pilih Kondisi</option>
|
||||||
|
<option value="sedang">Sedang</option>
|
||||||
|
<option value="buruk">Buruk</option>
|
||||||
|
<option value="sangat_buruk">Sangat Buruk</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
|
||||||
|
<textarea id="edit_rm_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button onclick="document.getElementById('modal_edit_miskin').close()" class="btn btn-ghost">Batal</button>
|
||||||
|
<button onclick="saveEditMiskin()" class="btn btn-error">
|
||||||
|
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Modal: Confirm Delete -->
|
<!-- Modal: Confirm Delete -->
|
||||||
<dialog id="deleteModal" class="modal">
|
<dialog id="deleteModal" class="modal">
|
||||||
<div class="modal-box max-w-sm text-center">
|
<div class="modal-box max-w-sm text-center">
|
||||||
@@ -175,6 +289,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const authUserRole = '{{ auth()->user()->getRoleNames()->first() ?? '' }}';
|
const authUserRole = '{{ auth()->user()->getRoleNames()->first() ?? '' }}';
|
||||||
|
const authUserId = {{ auth()->id() }};
|
||||||
const canEditIbadah = ['administrator', 'pengurus_ibadah'].includes(authUserRole);
|
const canEditIbadah = ['administrator', 'pengurus_ibadah'].includes(authUserRole);
|
||||||
const canEditMiskin = ['administrator', 'petugas_pendataan'].includes(authUserRole);
|
const canEditMiskin = ['administrator', 'petugas_pendataan'].includes(authUserRole);
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
@@ -206,7 +321,9 @@
|
|||||||
|
|
||||||
// Ibadah Table
|
// Ibadah Table
|
||||||
const bodyI = document.getElementById('bodyIbadah');
|
const bodyI = document.getElementById('bodyIbadah');
|
||||||
bodyI.innerHTML = dataI.length ? dataI.map((item, i) => `
|
bodyI.innerHTML = dataI.length ? dataI.map((item, i) => {
|
||||||
|
const canEditThis = canEditIbadah && (authUserRole === 'administrator' || item.user_id === authUserId);
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-xs font-bold opacity-30">${i + 1}</td>
|
<td class="text-xs font-bold opacity-30">${i + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -221,10 +338,13 @@
|
|||||||
<td><span class="badge badge-outline badge-sm">${item.radius}m</span></td>
|
<td><span class="badge badge-outline badge-sm">${item.radius}m</span></td>
|
||||||
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(4)}, ${parseFloat(item.longitude).toFixed(4)}</td>
|
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(4)}, ${parseFloat(item.longitude).toFixed(4)}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
${canEditIbadah ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})"><i class="fa-solid fa-trash"></i></button>` : ''}
|
<div class="flex gap-1 justify-center">
|
||||||
|
${canEditThis ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditIbadahById(${item.id})"><i class="fa-solid fa-pen"></i></button>` : ''}
|
||||||
|
${canEditThis ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})"><i class="fa-solid fa-trash"></i></button>` : ''}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>`;
|
||||||
`).join('') : '<tr><td colspan="7" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
|
}).join('') : '<tr><td colspan="7" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
|
||||||
|
|
||||||
// Miskin Table
|
// Miskin Table
|
||||||
const bodyM = document.getElementById('bodyMiskin');
|
const bodyM = document.getElementById('bodyMiskin');
|
||||||
@@ -243,7 +363,10 @@
|
|||||||
<td class="capitalize text-xs">${item.kondisi_rumah || '-'}</td>
|
<td class="capitalize text-xs">${item.kondisi_rumah || '-'}</td>
|
||||||
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(4)}, ${parseFloat(item.longitude).toFixed(4)}</td>
|
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(4)}, ${parseFloat(item.longitude).toFixed(4)}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
|
<div class="flex gap-1 justify-center">
|
||||||
|
${canEditMiskin ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditMiskinById('${item.id_rumah}')"><i class="fa-solid fa-pen"></i></button>` : ''}
|
||||||
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')"><i class="fa-solid fa-trash"></i></button>` : ''}
|
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')"><i class="fa-solid fa-trash"></i></button>` : ''}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('') : '<tr><td colspan="7" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
|
`).join('') : '<tr><td colspan="7" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
|
||||||
@@ -293,6 +416,107 @@
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── EDIT FORMS ──────────────────────────────────────────────
|
||||||
|
function openEditIbadahById(id) {
|
||||||
|
const item = ibadahData.find(i => i.id === id);
|
||||||
|
if (!item) return;
|
||||||
|
document.getElementById('edit_ib_id').value = item.id;
|
||||||
|
document.getElementById('edit_ib_nama').value = item.nama || '';
|
||||||
|
document.getElementById('edit_ib_jenis').value = item.jenis || '';
|
||||||
|
document.getElementById('edit_ib_alamat').value = item.alamat || '';
|
||||||
|
document.getElementById('edit_ib_pengurus').value = item.pengurus || '';
|
||||||
|
document.getElementById('edit_ib_telepon').value = item.telepon || '';
|
||||||
|
document.getElementById('edit_ib_jemaah').value = item.jumlah_jemaah || 0;
|
||||||
|
document.getElementById('edit_ib_radius').value = item.radius || 500;
|
||||||
|
document.getElementById('edit_ib_ket').value = item.keterangan || '';
|
||||||
|
document.getElementById('modal_edit_ibadah').showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditIbadah() {
|
||||||
|
const id = document.getElementById('edit_ib_id').value;
|
||||||
|
const item = ibadahData.find(i => i.id == id);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
nama: document.getElementById('edit_ib_nama').value,
|
||||||
|
jenis: document.getElementById('edit_ib_jenis').value,
|
||||||
|
alamat: document.getElementById('edit_ib_alamat').value,
|
||||||
|
pengurus: document.getElementById('edit_ib_pengurus').value,
|
||||||
|
telepon: document.getElementById('edit_ib_telepon').value,
|
||||||
|
jumlah_jemaah: parseInt(document.getElementById('edit_ib_jemaah').value) || 0,
|
||||||
|
radius: parseInt(document.getElementById('edit_ib_radius').value) || 500,
|
||||||
|
keterangan: document.getElementById('edit_ib_ket').value,
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`{{ url('/api/ibadah') }}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
if (resJson.success) {
|
||||||
|
document.getElementById('modal_edit_ibadah').close();
|
||||||
|
showToast('Data berhasil diperbarui', 'success');
|
||||||
|
await loadAllData();
|
||||||
|
} else if (res.status === 403) {
|
||||||
|
showToast(resJson.message || 'Tidak diizinkan mengubah data ini', 'error');
|
||||||
|
} else {
|
||||||
|
showToast('Gagal memperbarui data', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditMiskinById(idRumah) {
|
||||||
|
const item = miskinData.find(i => i.id_rumah === idRumah);
|
||||||
|
if (!item) return;
|
||||||
|
document.getElementById('edit_rm_id').value = item.id_rumah;
|
||||||
|
document.getElementById('edit_rm_nama_kk').value = item.nama_kk || '';
|
||||||
|
document.getElementById('edit_rm_kk').value = item.jumlah_kk || 1;
|
||||||
|
document.getElementById('edit_rm_jiwa').value = item.jumlah_orang || 4;
|
||||||
|
document.getElementById('edit_rm_alamat').value = item.alamat || '';
|
||||||
|
document.getElementById('edit_rm_kerja').value = item.pekerjaan || '';
|
||||||
|
document.getElementById('edit_rm_duit').value = item.pendapatan || 0;
|
||||||
|
document.getElementById('edit_rm_kondisi').value = item.kondisi_rumah || '';
|
||||||
|
document.getElementById('edit_rm_ket').value = item.keterangan || '';
|
||||||
|
document.getElementById('modal_edit_miskin').showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditMiskin() {
|
||||||
|
const idRumah = document.getElementById('edit_rm_id').value;
|
||||||
|
const item = miskinData.find(i => i.id_rumah === idRumah);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
nama_kk: document.getElementById('edit_rm_nama_kk').value,
|
||||||
|
jumlah_kk: parseInt(document.getElementById('edit_rm_kk').value) || 1,
|
||||||
|
jumlah_orang: parseInt(document.getElementById('edit_rm_jiwa').value) || 1,
|
||||||
|
alamat: document.getElementById('edit_rm_alamat').value,
|
||||||
|
pekerjaan: document.getElementById('edit_rm_kerja').value,
|
||||||
|
pendapatan: parseInt(document.getElementById('edit_rm_duit').value) || 0,
|
||||||
|
kondisi_rumah: document.getElementById('edit_rm_kondisi').value,
|
||||||
|
keterangan: document.getElementById('edit_rm_ket').value,
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`{{ url('/api/miskin') }}/${idRumah}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
if (resJson.success) {
|
||||||
|
document.getElementById('modal_edit_miskin').close();
|
||||||
|
showToast('Data berhasil diperbarui', 'success');
|
||||||
|
await loadAllData();
|
||||||
|
} else {
|
||||||
|
showToast('Gagal memperbarui data', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let deleteTarget = null;
|
let deleteTarget = null;
|
||||||
function prepDelete(type, id) {
|
function prepDelete(type, id) {
|
||||||
deleteTarget = { type, id };
|
deleteTarget = { type, id };
|
||||||
|
|||||||
@@ -256,6 +256,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Modal: Edit Rumah Ibadah -->
|
||||||
|
<dialog id="modal_edit_ibadah" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-lg">
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
<i class="fa-solid fa-mosque mr-2 text-success"></i>Edit Rumah Ibadah
|
||||||
|
</h3>
|
||||||
|
<input type="hidden" id="edit_ib_id">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Nama *</span></label>
|
||||||
|
<input id="edit_ib_nama" type="text" class="input input-bordered w-full" placeholder="Nama rumah ibadah">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jenis</span></label>
|
||||||
|
<select id="edit_ib_jenis" class="select select-bordered w-full">
|
||||||
|
<option value="">Pilih Jenis</option>
|
||||||
|
<option value="masjid">Masjid</option>
|
||||||
|
<option value="gereja">Gereja</option>
|
||||||
|
<option value="pura">Pura</option>
|
||||||
|
<option value="klenteng">Klenteng</option>
|
||||||
|
<option value="vihara">Vihara</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Radius Analisis (m)</span></label>
|
||||||
|
<input id="edit_ib_radius" type="number" class="input input-bordered w-full" value="500" min="50" max="2000">
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Alamat</span></label>
|
||||||
|
<textarea id="edit_ib_alamat" class="textarea textarea-bordered w-full" rows="2" placeholder="Alamat lengkap"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Pengurus</span></label>
|
||||||
|
<input id="edit_ib_pengurus" type="text" class="input input-bordered w-full" placeholder="Nama pengurus">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Telepon</span></label>
|
||||||
|
<input id="edit_ib_telepon" type="text" class="input input-bordered w-full" placeholder="No. telepon">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jumlah Jemaah</span></label>
|
||||||
|
<input id="edit_ib_jemaah" type="number" class="input input-bordered w-full" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
|
||||||
|
<textarea id="edit_ib_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button onclick="document.getElementById('modal_edit_ibadah').close()" class="btn btn-ghost">Batal</button>
|
||||||
|
<button onclick="saveEditIbadah()" class="btn btn-success">
|
||||||
|
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Modal: Input Miskin -->
|
<!-- Modal: Input Miskin -->
|
||||||
<dialog id="modalMiskin" class="modal">
|
<dialog id="modalMiskin" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
@@ -318,6 +376,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Modal: Edit Keluarga Miskin -->
|
||||||
|
<dialog id="modal_edit_miskin" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-lg">
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
<i class="fa-solid fa-house mr-2 text-error"></i>Edit Keluarga Miskin
|
||||||
|
</h3>
|
||||||
|
<input type="hidden" id="edit_rm_id">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Nama Kepala Keluarga</span></label>
|
||||||
|
<input id="edit_rm_nama_kk" type="text" class="input input-bordered w-full" placeholder="Nama KK">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jumlah KK</span></label>
|
||||||
|
<input id="edit_rm_kk" type="number" class="input input-bordered w-full" value="1" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Jumlah Jiwa</span></label>
|
||||||
|
<input id="edit_rm_jiwa" type="number" class="input input-bordered w-full" value="4" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Alamat</span></label>
|
||||||
|
<textarea id="edit_rm_alamat" class="textarea textarea-bordered w-full" rows="2" placeholder="Alamat lengkap"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Pekerjaan</span></label>
|
||||||
|
<input id="edit_rm_kerja" type="text" class="input input-bordered w-full" placeholder="Pekerjaan KK">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Pendapatan (Rp)</span></label>
|
||||||
|
<input id="edit_rm_duit" type="number" class="input input-bordered w-full" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label"><span class="label-text font-bold">Kondisi Rumah</span></label>
|
||||||
|
<select id="edit_rm_kondisi" class="select select-bordered w-full">
|
||||||
|
<option value="">Pilih Kondisi</option>
|
||||||
|
<option value="sedang">Sedang</option>
|
||||||
|
<option value="buruk">Buruk</option>
|
||||||
|
<option value="sangat_buruk">Sangat Buruk</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-control col-span-2">
|
||||||
|
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
|
||||||
|
<textarea id="edit_rm_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button onclick="document.getElementById('modal_edit_miskin').close()" class="btn btn-ghost">Batal</button>
|
||||||
|
<button onclick="saveEditMiskin()" class="btn btn-error">
|
||||||
|
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Modal: Confirm Delete -->
|
<!-- Modal: Confirm Delete -->
|
||||||
<dialog id="deleteModal" class="modal">
|
<dialog id="deleteModal" class="modal">
|
||||||
<div class="modal-box max-w-sm text-center">
|
<div class="modal-box max-w-sm text-center">
|
||||||
@@ -337,6 +451,7 @@
|
|||||||
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
|
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
|
||||||
<script>
|
<script>
|
||||||
const authUserRole = '{{ auth()->user()->getRoleNames()->first() ?? '' }}';
|
const authUserRole = '{{ auth()->user()->getRoleNames()->first() ?? '' }}';
|
||||||
|
const authUserId = {{ auth()->id() }};
|
||||||
const canEditIbadah = ['administrator', 'pengurus_ibadah'].includes(authUserRole);
|
const canEditIbadah = ['administrator', 'pengurus_ibadah'].includes(authUserRole);
|
||||||
const canEditMiskin = ['administrator', 'petugas_pendataan'].includes(authUserRole);
|
const canEditMiskin = ['administrator', 'petugas_pendataan'].includes(authUserRole);
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
@@ -633,6 +748,7 @@
|
|||||||
|
|
||||||
// ── POPUPS ──────────────────────────────────────────────────────────
|
// ── POPUPS ──────────────────────────────────────────────────────────
|
||||||
function buildPopupIbadah(item) {
|
function buildPopupIbadah(item) {
|
||||||
|
const canEditThis = canEditIbadah && (authUserRole === 'administrator' || item.user_id === authUserId);
|
||||||
return `<div class="p-2 min-w-[200px]">
|
return `<div class="p-2 min-w-[200px]">
|
||||||
<div class="font-bold text-base text-primary">🕌 ${item.nama}</div>
|
<div class="font-bold text-base text-primary">🕌 ${item.nama}</div>
|
||||||
<div class="badge badge-sm badge-outline mb-2 capitalize">${item.jenis || 'Ibadah'}</div>
|
<div class="badge badge-sm badge-outline mb-2 capitalize">${item.jenis || 'Ibadah'}</div>
|
||||||
@@ -642,7 +758,8 @@
|
|||||||
<div><strong>Jemaah:</strong><br>${item.jumlah_jemaah || 0}</div>
|
<div><strong>Jemaah:</strong><br>${item.jumlah_jemaah || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 border-t pt-2 mt-1">
|
<div class="flex gap-1 border-t pt-2 mt-1">
|
||||||
${canEditIbadah ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})">Hapus</button>` : ''}
|
${canEditThis ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditIbadahById(${item.id})"><i class="fa-solid fa-pen mr-1"></i>Edit</button>` : ''}
|
||||||
|
${canEditThis ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})">Hapus</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -657,6 +774,7 @@
|
|||||||
<div><strong>Kondisi:</strong><br><span class="capitalize">${item.kondisi_rumah || '-'}</span></div>
|
<div><strong>Kondisi:</strong><br><span class="capitalize">${item.kondisi_rumah || '-'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 border-t pt-2 mt-1">
|
<div class="flex gap-1 border-t pt-2 mt-1">
|
||||||
|
${canEditMiskin ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditMiskinById('${item.id_rumah}')"><i class="fa-solid fa-pen mr-1"></i>Edit</button>` : ''}
|
||||||
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')">Hapus</button>` : ''}
|
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')">Hapus</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -771,6 +889,107 @@
|
|||||||
} else showToast('Gagal menghapus', 'error');
|
} else showToast('Gagal menghapus', 'error');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── EDIT FORMS ─────────────────────────────────────────────
|
||||||
|
function openEditIbadahById(id) {
|
||||||
|
const item = ibadahData.find(i => i.id === id);
|
||||||
|
if (!item) return;
|
||||||
|
document.getElementById('edit_ib_id').value = item.id;
|
||||||
|
document.getElementById('edit_ib_nama').value = item.nama || '';
|
||||||
|
document.getElementById('edit_ib_jenis').value = item.jenis || '';
|
||||||
|
document.getElementById('edit_ib_alamat').value = item.alamat || '';
|
||||||
|
document.getElementById('edit_ib_pengurus').value = item.pengurus || '';
|
||||||
|
document.getElementById('edit_ib_telepon').value = item.telepon || '';
|
||||||
|
document.getElementById('edit_ib_jemaah').value = item.jumlah_jemaah || 0;
|
||||||
|
document.getElementById('edit_ib_radius').value = item.radius || 500;
|
||||||
|
document.getElementById('edit_ib_ket').value = item.keterangan || '';
|
||||||
|
document.getElementById('modal_edit_ibadah').showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditIbadah() {
|
||||||
|
const id = document.getElementById('edit_ib_id').value;
|
||||||
|
const item = ibadahData.find(i => i.id == id);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
nama: document.getElementById('edit_ib_nama').value,
|
||||||
|
jenis: document.getElementById('edit_ib_jenis').value,
|
||||||
|
alamat: document.getElementById('edit_ib_alamat').value,
|
||||||
|
pengurus: document.getElementById('edit_ib_pengurus').value,
|
||||||
|
telepon: document.getElementById('edit_ib_telepon').value,
|
||||||
|
jumlah_jemaah: parseInt(document.getElementById('edit_ib_jemaah').value) || 0,
|
||||||
|
radius: parseInt(document.getElementById('edit_ib_radius').value) || 500,
|
||||||
|
keterangan: document.getElementById('edit_ib_ket').value,
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`{{ url('/api/ibadah') }}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
if (resJson.success) {
|
||||||
|
document.getElementById('modal_edit_ibadah').close();
|
||||||
|
showToast('Data berhasil diperbarui', 'success');
|
||||||
|
await loadData();
|
||||||
|
} else if (res.status === 403) {
|
||||||
|
showToast(resJson.message || 'Tidak diizinkan mengubah data ini', 'error');
|
||||||
|
} else {
|
||||||
|
showToast('Gagal memperbarui data', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditMiskinById(idRumah) {
|
||||||
|
const item = miskinData.find(i => i.id_rumah === idRumah);
|
||||||
|
if (!item) return;
|
||||||
|
document.getElementById('edit_rm_id').value = item.id_rumah;
|
||||||
|
document.getElementById('edit_rm_nama_kk').value = item.nama_kk || '';
|
||||||
|
document.getElementById('edit_rm_kk').value = item.jumlah_kk || 1;
|
||||||
|
document.getElementById('edit_rm_jiwa').value = item.jumlah_orang || 4;
|
||||||
|
document.getElementById('edit_rm_alamat').value = item.alamat || '';
|
||||||
|
document.getElementById('edit_rm_kerja').value = item.pekerjaan || '';
|
||||||
|
document.getElementById('edit_rm_duit').value = item.pendapatan || 0;
|
||||||
|
document.getElementById('edit_rm_kondisi').value = item.kondisi_rumah || '';
|
||||||
|
document.getElementById('edit_rm_ket').value = item.keterangan || '';
|
||||||
|
document.getElementById('modal_edit_miskin').showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditMiskin() {
|
||||||
|
const idRumah = document.getElementById('edit_rm_id').value;
|
||||||
|
const item = miskinData.find(i => i.id_rumah === idRumah);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
nama_kk: document.getElementById('edit_rm_nama_kk').value,
|
||||||
|
jumlah_kk: parseInt(document.getElementById('edit_rm_kk').value) || 1,
|
||||||
|
jumlah_orang: parseInt(document.getElementById('edit_rm_jiwa').value) || 1,
|
||||||
|
alamat: document.getElementById('edit_rm_alamat').value,
|
||||||
|
pekerjaan: document.getElementById('edit_rm_kerja').value,
|
||||||
|
pendapatan: parseInt(document.getElementById('edit_rm_duit').value) || 0,
|
||||||
|
kondisi_rumah: document.getElementById('edit_rm_kondisi').value,
|
||||||
|
keterangan: document.getElementById('edit_rm_ket').value,
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`{{ url('/api/miskin') }}/${idRumah}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
if (resJson.success) {
|
||||||
|
document.getElementById('modal_edit_miskin').close();
|
||||||
|
showToast('Data berhasil diperbarui', 'success');
|
||||||
|
await loadData();
|
||||||
|
} else {
|
||||||
|
showToast('Gagal memperbarui data', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── TOAST ──────────────────────────────────────────────────────────
|
// ── TOAST ──────────────────────────────────────────────────────────
|
||||||
function showToast(msg, type = 'info') {
|
function showToast(msg, type = 'info') {
|
||||||
const t = document.createElement('div');
|
const t = document.createElement('div');
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\RumahIbadah;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class EditFormTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
foreach (['administrator', 'pengurus_ibadah', 'petugas_pendataan', 'pemangku_kebijakan'] as $role) {
|
||||||
|
\Spatie\Permission\Models\Role::create(['name' => $role, 'guard_name' => 'web']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createUser(string $role): User
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$user->assignRole($role);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createIbadah(?int $userId = null): RumahIbadah
|
||||||
|
{
|
||||||
|
return RumahIbadah::create([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'nama' => 'Test Masjid',
|
||||||
|
'jenis' => 'masjid',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
'radius' => 500,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_ibadah_store_sets_user_id(): void
|
||||||
|
{
|
||||||
|
$pengurus = $this->createUser('pengurus_ibadah');
|
||||||
|
$this->actingAs($pengurus);
|
||||||
|
|
||||||
|
$this->postJson('/api/ibadah', [
|
||||||
|
'nama' => 'Masjid Baru',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
])->assertOk()->assertJsonFragment(['user_id' => $pengurus->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pengurus_can_update_own_ibadah(): void
|
||||||
|
{
|
||||||
|
$pengurus = $this->createUser('pengurus_ibadah');
|
||||||
|
$ibadah = $this->createIbadah($pengurus->id);
|
||||||
|
$this->actingAs($pengurus);
|
||||||
|
|
||||||
|
$this->putJson("/api/ibadah/{$ibadah->id}", [
|
||||||
|
'nama' => 'Masjid Diperbarui',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
'radius' => 500,
|
||||||
|
])->assertOk()->assertJsonFragment(['nama' => 'Masjid Diperbarui']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pengurus_cannot_update_others_ibadah(): void
|
||||||
|
{
|
||||||
|
$pengurus = $this->createUser('pengurus_ibadah');
|
||||||
|
$admin = $this->createUser('administrator');
|
||||||
|
$ibadah = $this->createIbadah($admin->id);
|
||||||
|
$this->actingAs($pengurus);
|
||||||
|
|
||||||
|
$this->putJson("/api/ibadah/{$ibadah->id}", [
|
||||||
|
'nama' => 'Coba Edit',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
'radius' => 500,
|
||||||
|
])->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_administrator_can_update_any_ibadah(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('administrator');
|
||||||
|
$pengurus = $this->createUser('pengurus_ibadah');
|
||||||
|
$ibadah = $this->createIbadah($pengurus->id);
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
$this->putJson("/api/ibadah/{$ibadah->id}", [
|
||||||
|
'nama' => 'Edit oleh Admin',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
'radius' => 500,
|
||||||
|
])->assertOk()->assertJsonFragment(['nama' => 'Edit oleh Admin']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pengurus_cannot_delete_others_ibadah(): void
|
||||||
|
{
|
||||||
|
$pengurus = $this->createUser('pengurus_ibadah');
|
||||||
|
$admin = $this->createUser('administrator');
|
||||||
|
$ibadah = $this->createIbadah($admin->id);
|
||||||
|
$this->actingAs($pengurus);
|
||||||
|
|
||||||
|
$this->deleteJson("/api/ibadah/{$ibadah->id}")->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pengurus_can_delete_own_ibadah(): void
|
||||||
|
{
|
||||||
|
$pengurus = $this->createUser('pengurus_ibadah');
|
||||||
|
$ibadah = $this->createIbadah($pengurus->id);
|
||||||
|
$this->actingAs($pengurus);
|
||||||
|
|
||||||
|
$this->deleteJson("/api/ibadah/{$ibadah->id}")->assertOk();
|
||||||
|
$this->assertNull(RumahIbadah::find($ibadah->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_petugas_can_update_all_miskin_fields(): void
|
||||||
|
{
|
||||||
|
$petugas = $this->createUser('petugas_pendataan');
|
||||||
|
$this->actingAs($petugas);
|
||||||
|
|
||||||
|
$this->postJson('/api/miskin', [
|
||||||
|
'id_rumah' => 'TESTID001',
|
||||||
|
'nama_kk' => 'Budi Santoso',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
'jumlah_kk' => 1,
|
||||||
|
'jumlah_orang' => 4,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$this->putJson('/api/miskin/TESTID001', [
|
||||||
|
'nama_kk' => 'Budi Diperbarui',
|
||||||
|
'latitude' => -7.0,
|
||||||
|
'longitude' => 110.0,
|
||||||
|
'jumlah_kk' => 2,
|
||||||
|
'jumlah_orang' => 6,
|
||||||
|
'pekerjaan' => 'Buruh',
|
||||||
|
'pendapatan' => 1500000,
|
||||||
|
'kondisi_rumah' => 'buruk',
|
||||||
|
'keterangan' => 'Catatan baru',
|
||||||
|
])->assertOk()->assertJsonFragment(['nama_kk' => 'Budi Diperbarui']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,10 @@ class ExampleTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* A basic test example.
|
* A basic test example.
|
||||||
*/
|
*/
|
||||||
public function test_the_application_returns_a_successful_response(): void
|
public function test_unauthenticated_root_redirects_to_login(): void
|
||||||
{
|
{
|
||||||
$response = $this->get('/');
|
$response = $this->get('/');
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertRedirect('/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user