chore: remove docs and tests from deploy package
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
# Docker Development
|
||||
|
||||
This setup runs the Laravel app, Vite, and MySQL through Docker Compose.
|
||||
|
||||
## Services
|
||||
|
||||
- Laravel app: `http://localhost:8000`
|
||||
- Vite dev server: `http://localhost:5173`
|
||||
- MySQL from host: `127.0.0.1:3307`
|
||||
- MySQL from containers: `mysql:3306`
|
||||
|
||||
## First Run
|
||||
|
||||
If you want Docker to own the Laravel environment values, copy the Docker example:
|
||||
|
||||
```powershell
|
||||
Copy-Item .env.docker.example .env
|
||||
```
|
||||
|
||||
Then start everything:
|
||||
|
||||
```powershell
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
The app container installs Composer dependencies, generates `APP_KEY` if needed, clears config, runs migrations, and seeds the database (roles + default admin account).
|
||||
|
||||
The Vite container installs Node dependencies and starts the dev server.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```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 test
|
||||
docker compose down
|
||||
```
|
||||
|
||||
To remove the MySQL data volume and start with a clean database:
|
||||
|
||||
```powershell
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## Database Credentials
|
||||
|
||||
```text
|
||||
Database: webgis_miskin
|
||||
Username: webgis
|
||||
Password: secret
|
||||
Root password: root
|
||||
```
|
||||
|
||||
## Default Login
|
||||
|
||||
Akses `http://localhost:8000` dan login dengan salah satu akun berikut:
|
||||
|
||||
| Email | Password | Role |
|
||||
|---|---|---|
|
||||
| `admin@webgis.local` | `admin123` | Administrator (akses penuh) |
|
||||
| `pengurus@webgis.local` | `password123` | Pengurus Ibadah |
|
||||
| `petugas@webgis.local` | `password123` | Petugas Pendataan |
|
||||
| `kebijakan@webgis.local` | `password123` | Pemangku Kebijakan (view only) |
|
||||
|
||||
@@ -1,746 +0,0 @@
|
||||
# Plan 3: Manajemen Pengguna (Admin User Management)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Tambah halaman admin `/admin/users` untuk CRUD pengguna dan assign role — hanya bisa diakses oleh `administrator`.
|
||||
|
||||
**Architecture:** Buat `AdminUserController` dengan 5 endpoint JSON (`index`, `store`, `update`, `destroy`, `resetPassword`). Buat view `resources/views/admin/users.blade.php` dengan pola DaisyUI yang sama persis seperti `data.blade.php` (fetch API, toast, modals, table rows). Tambahkan link "Admin" ke navbar di `map.blade.php` dan `data.blade.php` — muncul hanya untuk role `administrator`. Semua route di-protect dengan `role:administrator`.
|
||||
|
||||
**Tech Stack:** Laravel 13, Spatie Permission v8, Blade, Vanilla JS (fetch + async/await), DaisyUI 4, Tailwind CSS, Font Awesome 6.
|
||||
|
||||
> **Scope note:** Ini adalah Plan 3 dari `missing_features.md` (A-01). Plan 4–6 adalah subsistem terpisah yang masing-masing butuh plan tersendiri.
|
||||
|
||||
---
|
||||
|
||||
## Progress
|
||||
|
||||
| Task | Status |
|
||||
|------|--------|
|
||||
| Task 1: AdminUserController | ✅ Selesai (file dibuat, belum di-commit) |
|
||||
| Task 2: Routes | ⚠️ Sebagian — `use` statement ditambah, route group belum |
|
||||
| Task 3: View admin/users.blade.php | ❌ Belum |
|
||||
| Task 4: Navbar links (map + data) | ❌ Belum |
|
||||
| Task 5: Tests | ❌ Belum |
|
||||
| Task 6: Docs | ❌ Belum |
|
||||
|
||||
**Commit dulu sebelum lanjut:**
|
||||
```bash
|
||||
git add app/Http/Controllers/AdminUserController.php routes/web.php
|
||||
git commit -m "feat: add AdminUserController, add use statement to routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Create | `app/Http/Controllers/AdminUserController.php` |
|
||||
| Modify | `routes/web.php` |
|
||||
| Create | `resources/views/admin/users.blade.php` |
|
||||
| Modify | `resources/views/map.blade.php` |
|
||||
| Modify | `resources/views/data.blade.php` |
|
||||
| Create | `tests/Feature/AdminUserTest.php` |
|
||||
| Modify | `missing_features.md` |
|
||||
| Modify | `README.md` |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Buat AdminUserController ✅
|
||||
|
||||
File sudah dibuat di `app/Http/Controllers/AdminUserController.php`.
|
||||
|
||||
- [x] **Step 1: Tulis controller**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdminUserController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$users = User::with('roles')->get()->map(fn($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
'role' => $u->getRoleNames()->first() ?? null,
|
||||
]);
|
||||
return response()->json($users);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8',
|
||||
'role' => 'required|string|exists:roles,name',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => $validated['password'],
|
||||
]);
|
||||
$user->syncRoles([$validated['role']]);
|
||||
|
||||
return response()->json(['success' => true, 'data' => [
|
||||
'id' => $user->id, 'name' => $user->name,
|
||||
'email' => $user->email, 'role' => $validated['role'],
|
||||
]]);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . $id,
|
||||
'role' => 'required|string|exists:roles,name',
|
||||
]);
|
||||
|
||||
$user->update(['name' => $validated['name'], 'email' => $validated['email']]);
|
||||
$user->syncRoles([$validated['role']]);
|
||||
|
||||
return response()->json(['success' => true, 'data' => [
|
||||
'id' => $user->id, 'name' => $user->name,
|
||||
'email' => $user->email, 'role' => $validated['role'],
|
||||
]]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return response()->json(['message' => 'Tidak dapat menghapus akun sendiri.'], 403);
|
||||
}
|
||||
|
||||
$user->delete();
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function resetPassword(Request $request, $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
$user->update(['password' => $request->password]);
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/Http/Controllers/AdminUserController.php
|
||||
git commit -m "feat: add AdminUserController with CRUD and password reset"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Tambah Routes Admin
|
||||
|
||||
**Files:**
|
||||
- Modify: `routes/web.php`
|
||||
|
||||
> **STATUS:** `use App\Http\Controllers\AdminUserController;` sudah ditambah di routes/web.php. Tinggal tambahkan route group di Step 1 lalu commit.
|
||||
|
||||
- [ ] **Step 1: Tambah route group admin**
|
||||
|
||||
Di `routes/web.php`, dalam middleware group `auth`, tambahkan **sebelum** kurung tutup `});` terakhir:
|
||||
|
||||
```php
|
||||
// Admin routes — hanya administrator
|
||||
Route::middleware('role:administrator')->group(function () {
|
||||
Route::get('/admin/users', fn() => view('admin.users'))->name('admin.users');
|
||||
|
||||
Route::prefix('api/admin')->group(function () {
|
||||
Route::get('/users', [AdminUserController::class, 'index']);
|
||||
Route::post('/users', [AdminUserController::class, 'store']);
|
||||
Route::put('/users/{id}', [AdminUserController::class, 'update']);
|
||||
Route::delete('/users/{id}', [AdminUserController::class, 'destroy']);
|
||||
Route::put('/users/{id}/password', [AdminUserController::class, 'resetPassword']);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add routes/web.php
|
||||
git commit -m "feat: add admin user management routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Buat View Admin Users
|
||||
|
||||
**Files:**
|
||||
- Create: `resources/views/admin/users.blade.php`
|
||||
|
||||
Ikuti pola `data.blade.php` (sticky navbar, DaisyUI, JS fetch, toast, modals).
|
||||
|
||||
- [ ] **Step 1: Tulis view lengkap**
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Manajemen Pengguna - WebGIS</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
body { font-weight: 500; background-color: #f8fafc; }
|
||||
h1, h2, h3, h4, h5, h6, .btn, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
|
||||
<div class="flex-1">
|
||||
<a href="{{ url('/') }}" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
|
||||
<i class="fa-solid fa-house-chimney-crack"></i>
|
||||
WebGIS Pemetaan Kemiskinan
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<ul class="menu menu-horizontal px-1 gap-1">
|
||||
<li><a href="{{ url('/map') }}" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
|
||||
<li><a href="{{ url('/data') }}" class="font-medium"><i class="fa-solid fa-table-list"></i> Data</a></li>
|
||||
<li><a href="{{ url('/admin/users') }}" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-users-gear"></i> Admin</a></li>
|
||||
</ul>
|
||||
<div class="dropdown dropdown-end ml-2">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost gap-2">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-8">
|
||||
<span class="text-xs font-bold">{{ substr(auth()->user()->name, 0, 1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hidden sm:inline text-sm font-semibold">{{ auth()->user()->name }}</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs opacity-50"></i>
|
||||
</div>
|
||||
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-200 mt-2">
|
||||
<li class="menu-title">
|
||||
<span class="text-xs text-base-content/50 capitalize">{{ str_replace('_', ' ', auth()->user()->getRoleNames()->first() ?? 'user') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="text-error w-full text-left flex items-center gap-2">
|
||||
<i class="fa-solid fa-right-from-bracket"></i> Keluar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN -->
|
||||
<main class="max-w-screen-xl mx-auto px-4 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-3">
|
||||
<i class="fa-solid fa-users-gear text-primary"></i>
|
||||
Manajemen Pengguna
|
||||
</h1>
|
||||
<p class="text-base-content/50 mt-1">Kelola akun pengguna dan hak akses sistem.</p>
|
||||
</div>
|
||||
<button onclick="openCreateModal()" class="btn btn-primary shadow-lg gap-2">
|
||||
<i class="fa-solid fa-user-plus"></i> Tambah Pengguna
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8" id="statsGrid"></div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-base-200 overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr class="text-xs font-bold uppercase opacity-50">
|
||||
<th>#</th><th>Nama</th><th>Email</th><th>Role</th>
|
||||
<th class="text-center">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bodyUsers"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal: Tambah Pengguna -->
|
||||
<dialog id="modal_create_user" class="modal">
|
||||
<div class="modal-box w-11/12 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4"><i class="fa-solid fa-user-plus mr-2 text-primary"></i>Tambah Pengguna Baru</h3>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Nama Lengkap *</span></label>
|
||||
<input id="create_name" type="text" class="input input-bordered w-full" placeholder="Nama pengguna">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Email *</span></label>
|
||||
<input id="create_email" type="email" class="input input-bordered w-full" placeholder="email@domain.com">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Password *</span></label>
|
||||
<input id="create_password" type="password" class="input input-bordered w-full" placeholder="Min. 8 karakter">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Role *</span></label>
|
||||
<select id="create_role" class="select select-bordered w-full">
|
||||
<option value="administrator">Administrator</option>
|
||||
<option value="pengurus_ibadah">Pengurus Ibadah</option>
|
||||
<option value="petugas_pendataan" selected>Petugas Pendataan</option>
|
||||
<option value="pemangku_kebijakan">Pemangku Kebijakan</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button onclick="document.getElementById('modal_create_user').close()" class="btn btn-ghost">Batal</button>
|
||||
<button onclick="saveCreateUser()" class="btn btn-primary"><i class="fa-solid fa-floppy-disk mr-1"></i>Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||
</dialog>
|
||||
|
||||
<!-- Modal: Edit Pengguna -->
|
||||
<dialog id="modal_edit_user" class="modal">
|
||||
<div class="modal-box w-11/12 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4"><i class="fa-solid fa-user-pen mr-2 text-warning"></i>Edit Pengguna</h3>
|
||||
<input type="hidden" id="edit_user_id">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Nama Lengkap *</span></label>
|
||||
<input id="edit_name" type="text" class="input input-bordered w-full">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Email *</span></label>
|
||||
<input id="edit_email" type="email" class="input input-bordered w-full">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Role *</span></label>
|
||||
<select id="edit_role" class="select select-bordered w-full">
|
||||
<option value="administrator">Administrator</option>
|
||||
<option value="pengurus_ibadah">Pengurus Ibadah</option>
|
||||
<option value="petugas_pendataan">Petugas Pendataan</option>
|
||||
<option value="pemangku_kebijakan">Pemangku Kebijakan</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button onclick="document.getElementById('modal_edit_user').close()" class="btn btn-ghost">Batal</button>
|
||||
<button onclick="saveEditUser()" class="btn btn-warning"><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: Reset Password -->
|
||||
<dialog id="modal_reset_password" class="modal">
|
||||
<div class="modal-box w-11/12 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4"><i class="fa-solid fa-key mr-2 text-info"></i>Reset Password</h3>
|
||||
<input type="hidden" id="reset_user_id">
|
||||
<p class="text-sm text-base-content/60 mb-4" id="reset_user_label"></p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Password Baru *</span></label>
|
||||
<input id="reset_password" type="password" class="input input-bordered w-full" placeholder="Min. 8 karakter">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-bold">Konfirmasi Password *</span></label>
|
||||
<input id="reset_password_confirmation" type="password" class="input input-bordered w-full" placeholder="Ulangi password baru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button onclick="document.getElementById('modal_reset_password').close()" class="btn btn-ghost">Batal</button>
|
||||
<button onclick="saveResetPassword()" class="btn btn-info text-white"><i class="fa-solid fa-key mr-1"></i>Reset Password</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||
</dialog>
|
||||
|
||||
<!-- Modal: Confirm Delete -->
|
||||
<dialog id="modal_delete_user" class="modal">
|
||||
<div class="modal-box max-w-sm text-center">
|
||||
<div class="text-5xl mb-4 text-error"><i class="fa-solid fa-trash-can"></i></div>
|
||||
<h3 class="font-bold text-xl mb-2">Hapus Pengguna?</h3>
|
||||
<p class="text-sm text-base-content/60 mb-6" id="delete_user_label"></p>
|
||||
<input type="hidden" id="delete_user_id">
|
||||
<div class="flex gap-2 justify-center">
|
||||
<button class="btn btn-ghost px-8" onclick="document.getElementById('modal_delete_user').close()">Batal</button>
|
||||
<button class="btn btn-error px-8 text-white" onclick="confirmDeleteUser()">Ya, Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
|
||||
|
||||
<script>
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
let usersData = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadUsers);
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetch('{{ url('/api/admin/users') }}', { headers: { 'X-CSRF-TOKEN': csrfToken } });
|
||||
usersData = await res.json();
|
||||
renderUsers();
|
||||
} catch (e) { showToast('Gagal memuat data pengguna', 'error'); }
|
||||
}
|
||||
|
||||
const roleLabels = { administrator: 'Administrator', pengurus_ibadah: 'Pengurus Ibadah', petugas_pendataan: 'Petugas Pendataan', pemangku_kebijakan: 'Pemangku Kebijakan' };
|
||||
const roleBadge = { administrator: 'badge-error', pengurus_ibadah: 'badge-success', petugas_pendataan: 'badge-warning', pemangku_kebijakan: 'badge-info' };
|
||||
|
||||
function renderUsers() {
|
||||
const counts = { administrator: 0, pengurus_ibadah: 0, petugas_pendataan: 0, pemangku_kebijakan: 0 };
|
||||
usersData.forEach(u => { if (counts[u.role] !== undefined) counts[u.role]++; });
|
||||
document.getElementById('statsGrid').innerHTML = Object.entries(roleLabels).map(([key, label]) => `
|
||||
<div class="stat bg-white shadow rounded-2xl border border-base-200">
|
||||
<div class="stat-title text-xs uppercase font-bold opacity-50">${label}</div>
|
||||
<div class="stat-value text-2xl">${counts[key]}</div>
|
||||
</div>`).join('');
|
||||
|
||||
document.getElementById('bodyUsers').innerHTML = usersData.length ? usersData.map((u, i) => `
|
||||
<tr>
|
||||
<td class="text-xs font-bold opacity-30">${i + 1}</td>
|
||||
<td><div class="font-bold">${u.name}</div></td>
|
||||
<td class="text-sm opacity-70">${u.email}</td>
|
||||
<td><span class="badge badge-sm ${roleBadge[u.role] || 'badge-neutral'} badge-outline capitalize">${roleLabels[u.role] || u.role}</span></td>
|
||||
<td class="text-center">
|
||||
<div class="flex gap-1 justify-center">
|
||||
<button onclick="openEditModal(${u.id})" class="btn btn-xs btn-warning btn-outline" title="Edit"><i class="fa-solid fa-pen"></i></button>
|
||||
<button onclick="openResetModal(${u.id})" class="btn btn-xs btn-info btn-outline" title="Reset Password"><i class="fa-solid fa-key"></i></button>
|
||||
<button onclick="openDeleteModal(${u.id})" class="btn btn-xs btn-error btn-outline" title="Hapus"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('') : '<tr><td colspan="5" class="text-center py-10 opacity-30 italic">Belum ada pengguna</td></tr>';
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
['create_name','create_email','create_password'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('create_role').value = 'petugas_pendataan';
|
||||
document.getElementById('modal_create_user').showModal();
|
||||
}
|
||||
|
||||
async function saveCreateUser() {
|
||||
const body = { name: document.getElementById('create_name').value, email: document.getElementById('create_email').value, password: document.getElementById('create_password').value, role: document.getElementById('create_role').value };
|
||||
const res = await fetch('{{ url('/api/admin/users') }}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify(body) });
|
||||
const json = await res.json();
|
||||
if (json.success) { document.getElementById('modal_create_user').close(); showToast('Pengguna berhasil ditambahkan', 'success'); await loadUsers(); }
|
||||
else showToast(json.message || 'Gagal menambahkan pengguna', 'error');
|
||||
}
|
||||
|
||||
function openEditModal(id) {
|
||||
const u = usersData.find(u => u.id === id); if (!u) return;
|
||||
document.getElementById('edit_user_id').value = u.id;
|
||||
document.getElementById('edit_name').value = u.name;
|
||||
document.getElementById('edit_email').value = u.email;
|
||||
document.getElementById('edit_role').value = u.role || 'petugas_pendataan';
|
||||
document.getElementById('modal_edit_user').showModal();
|
||||
}
|
||||
|
||||
async function saveEditUser() {
|
||||
const id = document.getElementById('edit_user_id').value;
|
||||
const body = { name: document.getElementById('edit_name').value, email: document.getElementById('edit_email').value, role: document.getElementById('edit_role').value };
|
||||
const res = await fetch(`{{ url('/api/admin/users') }}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify(body) });
|
||||
const json = await res.json();
|
||||
if (json.success) { document.getElementById('modal_edit_user').close(); showToast('Data pengguna diperbarui', 'success'); await loadUsers(); }
|
||||
else showToast(json.message || 'Gagal memperbarui data', 'error');
|
||||
}
|
||||
|
||||
function openResetModal(id) {
|
||||
const u = usersData.find(u => u.id === id); if (!u) return;
|
||||
document.getElementById('reset_user_id').value = u.id;
|
||||
document.getElementById('reset_user_label').textContent = `Reset password untuk: ${u.name} (${u.email})`;
|
||||
document.getElementById('reset_password').value = '';
|
||||
document.getElementById('reset_password_confirmation').value = '';
|
||||
document.getElementById('modal_reset_password').showModal();
|
||||
}
|
||||
|
||||
async function saveResetPassword() {
|
||||
const id = document.getElementById('reset_user_id').value;
|
||||
const body = { password: document.getElementById('reset_password').value, password_confirmation: document.getElementById('reset_password_confirmation').value };
|
||||
const res = await fetch(`{{ url('/api/admin/users') }}/${id}/password`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify(body) });
|
||||
const json = await res.json();
|
||||
if (json.success) { document.getElementById('modal_reset_password').close(); showToast('Password berhasil direset', 'success'); }
|
||||
else showToast(json.message || 'Gagal reset password', 'error');
|
||||
}
|
||||
|
||||
function openDeleteModal(id) {
|
||||
const u = usersData.find(u => u.id === id); if (!u) return;
|
||||
document.getElementById('delete_user_id').value = u.id;
|
||||
document.getElementById('delete_user_label').textContent = `Pengguna "${u.name}" (${u.email}) akan dihapus secara permanen.`;
|
||||
document.getElementById('modal_delete_user').showModal();
|
||||
}
|
||||
|
||||
async function confirmDeleteUser() {
|
||||
const id = document.getElementById('delete_user_id').value;
|
||||
const res = await fetch(`{{ url('/api/admin/users') }}/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken } });
|
||||
const json = await res.json();
|
||||
if (json.success) { document.getElementById('modal_delete_user').close(); showToast('Pengguna dihapus', 'success'); await loadUsers(); }
|
||||
else showToast(json.message || 'Gagal menghapus pengguna', 'error');
|
||||
}
|
||||
|
||||
function showToast(msg, type = 'info') {
|
||||
const t = document.createElement('div');
|
||||
const cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
|
||||
t.className = `alert ${cls} shadow-lg py-3 text-xs font-bold`;
|
||||
t.innerHTML = `<span><i class="fa-solid fa-circle-info mr-2"></i>${msg}</span>`;
|
||||
document.getElementById('toastBox').appendChild(t);
|
||||
setTimeout(() => t.remove(), 3000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add resources/views/admin/users.blade.php
|
||||
git commit -m "feat: add admin users management view"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Tambah Link Admin ke Navbar
|
||||
|
||||
**Files:**
|
||||
- Modify: `resources/views/map.blade.php`
|
||||
- Modify: `resources/views/data.blade.php`
|
||||
|
||||
- [ ] **Step 1: Tambah di map.blade.php**
|
||||
|
||||
Cari `<ul class="menu menu-horizontal px-1 gap-1">` di `map.blade.php`, tambahkan setelah item "Data Tabular":
|
||||
|
||||
```html
|
||||
@if(auth()->user()->hasRole('administrator'))
|
||||
<li>
|
||||
<a href="{{ url('/admin/users') }}" class="font-medium">
|
||||
<i class="fa-solid fa-users-gear"></i> Admin
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Tambah di data.blade.php**
|
||||
|
||||
Sama persis, setelah item "Data Tabular" di `data.blade.php`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add resources/views/map.blade.php resources/views/data.blade.php
|
||||
git commit -m "feat: add Admin nav link for administrator role in map and data views"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Tulis Tests AdminUserController
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/Feature/AdminUserTest.php`
|
||||
|
||||
- [ ] **Step 1: Tulis test file**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminUserTest 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;
|
||||
}
|
||||
|
||||
public function test_admin_users_page_is_accessible(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
$this->get('/admin/users')->assertOk();
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_access_users_page(): void
|
||||
{
|
||||
$pengurus = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($pengurus);
|
||||
$this->get('/admin/users')->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_admin_can_list_users(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
$this->getJson('/api/admin/users')->assertOk()->assertJsonFragment(['email' => $admin->email]);
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_list_users(): void
|
||||
{
|
||||
$pengurus = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($pengurus);
|
||||
$this->getJson('/api/admin/users')->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_admin_can_create_user(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
$this->postJson('/api/admin/users', [
|
||||
'name' => 'User Baru', 'email' => 'userbaru@webgis.local',
|
||||
'password' => 'password123', 'role' => 'petugas_pendataan',
|
||||
])->assertOk()->assertJsonFragment(['email' => 'userbaru@webgis.local']);
|
||||
$this->assertDatabaseHas('users', ['email' => 'userbaru@webgis.local']);
|
||||
}
|
||||
|
||||
public function test_admin_can_update_user(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$target = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($admin);
|
||||
$this->putJson("/api/admin/users/{$target->id}", [
|
||||
'name' => 'Nama Diubah', 'email' => $target->email, 'role' => 'petugas_pendataan',
|
||||
])->assertOk()->assertJsonFragment(['name' => 'Nama Diubah']);
|
||||
$this->assertTrue($target->fresh()->hasRole('petugas_pendataan'));
|
||||
}
|
||||
|
||||
public function test_admin_can_delete_other_user(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$target = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($admin);
|
||||
$this->deleteJson("/api/admin/users/{$target->id}")->assertOk();
|
||||
$this->assertNull(User::find($target->id));
|
||||
}
|
||||
|
||||
public function test_admin_cannot_delete_self(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
$this->deleteJson("/api/admin/users/{$admin->id}")->assertStatus(403);
|
||||
$this->assertNotNull(User::find($admin->id));
|
||||
}
|
||||
|
||||
public function test_admin_can_reset_password(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$target = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($admin);
|
||||
$this->putJson("/api/admin/users/{$target->id}/password", [
|
||||
'password' => 'newpassword123', 'password_confirmation' => 'newpassword123',
|
||||
])->assertOk();
|
||||
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('newpassword123', $target->fresh()->password));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Jalankan tests**
|
||||
|
||||
```powershell
|
||||
docker compose exec app php artisan test tests/Feature/AdminUserTest.php --no-coverage
|
||||
```
|
||||
|
||||
Expected: Semua 8 tests **PASS**.
|
||||
|
||||
- [ ] **Step 3: Full test suite**
|
||||
|
||||
```powershell
|
||||
docker compose exec app php artisan test --no-coverage
|
||||
```
|
||||
|
||||
Expected: 28 tests **PASS**.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/Feature/AdminUserTest.php
|
||||
git commit -m "test: add AdminUserController tests (CRUD + delete-self guard + password reset)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update Docs
|
||||
|
||||
- [ ] **Step 1: Update missing_features.md**
|
||||
- "Terakhir diperbarui" → `2026-06-03 — setelah implementasi Manajemen Pengguna (Plan 3).`
|
||||
- `A-01 Manajemen Pengguna` → `✅ Selesai`
|
||||
- Ringkasan: Fitur Admin `✅ 1/8`, Total sisa `~13 item`
|
||||
- Plan order: `~~Plan 3~~ | ~~Manajemen Pengguna~~ | ✅ Selesai`
|
||||
|
||||
- [ ] **Step 2: Update README.md**
|
||||
|
||||
Di bagian "Daftar Halaman", tambahkan:
|
||||
```markdown
|
||||
4. **Admin Pengguna (`/admin/users`)**: Manajemen pengguna sistem — tambah, edit, hapus akun, assign role, dan reset password. Hanya dapat diakses oleh Administrator.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add missing_features.md README.md
|
||||
git commit -m "docs: mark Plan 3 complete — admin user management implemented, update README"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
1. Login `pengurus@webgis.local` → `/admin/users` → 403
|
||||
2. Login `admin@webgis.local` → `/admin/users` → halaman tampil
|
||||
3. Tambah pengguna → user muncul di tabel
|
||||
4. Edit pengguna → nama/role ter-update
|
||||
5. Reset password → bisa login dengan password baru
|
||||
6. Hapus pengguna lain → hilang dari tabel
|
||||
7. Hapus akun sendiri → toast error
|
||||
8. Navbar: link "Admin" hanya muncul untuk administrator
|
||||
|
||||
```powershell
|
||||
docker compose exec app php artisan test --no-coverage
|
||||
# Expected: 28 tests PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **Plans 4–6:**
|
||||
> - Plan 4: Dashboard Statistik + Gap Analysis
|
||||
> - Plan 5: Ekspor PDF + per-radius export (`barryvdh/laravel-dompdf`)
|
||||
> - Plan 6: Bulk delete, CSV import, log aktivitas, backup
|
||||
@@ -1,200 +0,0 @@
|
||||
# Fitur & Role yang Belum Ada di Project WebGIS
|
||||
|
||||
> Berdasarkan perbandingan dokumen SKPL (`D1041231009_RadityaIndraPutranto_SKPL_SIG.docx`) dengan implementasi aktual project.
|
||||
>
|
||||
> **Terakhir diperbarui:** 2026-06-05 — ditambahkan saran fitur dari dosen (anggota keluarga, riwayat bantuan, permohonan transfer).
|
||||
|
||||
---
|
||||
|
||||
## 🔐 1. Sistem Autentikasi & Role
|
||||
|
||||
### 1.1 Komponen:
|
||||
|
||||
- [x] **Model User** — `App\Models\User` dengan `HasRoles` (Spatie)
|
||||
- [x] **Migration `users`** — Tabel users + password_reset_tokens
|
||||
- [x] **Halaman Login** — `resources/views/auth/login.blade.php`
|
||||
- [x] **AuthController** — Login/logout dengan session regeneration
|
||||
- [x] **Middleware Auth** — Semua route dilindungi, API di-enforce di backend
|
||||
- [x] **Role-Based Access Control (RBAC)** — Spatie Laravel Permission v8, 4 role aktif
|
||||
|
||||
### 1.2 Empat role:
|
||||
|
||||
| # | Role | Kode SKPL | Status |
|
||||
|---|---|---|---|
|
||||
| 1 | **Administrator Sistem** | A-01 s/d A-05 | ⚠️ A-01 selesai, A-02–A-05 belum (lihat bagian 2) |
|
||||
| 2 | **Pengurus Rumah Ibadah** | I-01 s/d I-06 | ⚠️ Bisa CRUD ibadah, tapi belum ada ownership |
|
||||
| 3 | **Petugas Pendataan (Operator)** | P-01 s/d P-05 | ⚠️ Bisa CRUD miskin, belum ada form edit & verifikasi |
|
||||
| 4 | **Pemangku Kebijakan** | K-01 s/d K-04 | ⚠️ View-only sudah, dashboard & ekspor lanjutan belum |
|
||||
|
||||
### 1.3 Gap ownership yang ditemukan saat implementasi:
|
||||
|
||||
- [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.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 2. Fitur Administrator (A-01 s/d A-05)
|
||||
|
||||
| Kode | Fitur | Status |
|
||||
|---|---|---|
|
||||
| A-01 | **Manajemen Pengguna** — CRUD user, assign role, reset password | ✅ Selesai |
|
||||
| A-02 | **Bulk Delete** — Hapus data massal | ✅ Selesai |
|
||||
| A-02 | **Import CSV** — Import data dari file CSV | ✅ Selesai |
|
||||
| A-03 | **Konfigurasi Sistem** — Batas wilayah, radius min/max, legenda peta | ✅ Selesai (center peta, zoom, radius min/max) |
|
||||
| A-04 | **Log Aktivitas** — Mencatat dan menampilkan log aktivitas pengguna | ✅ Selesai |
|
||||
| A-04 | **Statistik Penggunaan** — Ringkasan penggunaan sistem | ✅ Selesai |
|
||||
| A-04 | **Notifikasi Perubahan Data** — Notif jika data ditambah/diubah | ✅ Selesai (polling 30 detik) |
|
||||
| A-05 | **Backup & Restore** — Backup database manual, jadwal backup otomatis | ✅ Selesai (backup manual + terjadwal harian + restore dari file) |
|
||||
|
||||
---
|
||||
|
||||
## ✏️ 3. Fitur Edit Data (Multiple Roles)
|
||||
|
||||
Data bisa ditambah dan dihapus. Untuk lokasi: drag marker (ibadah & miskin) dan ubah radius (ibadah saja) sudah bisa. Namun **belum ada form edit** untuk field lainnya.
|
||||
|
||||
| Fitur | Role terkait | Status |
|
||||
|---|---|---|
|
||||
| **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 | ✅ Selesai |
|
||||
|
||||
---
|
||||
|
||||
## 📊 4. Fitur Analisis & Dashboard (Pemangku Kebijakan)
|
||||
|
||||
| Kode | Fitur | Status |
|
||||
|---|---|---|
|
||||
| K-02 | **Dashboard Statistik Lengkap** — Perbandingan antar wilayah, tren waktu | ✅ Selesai |
|
||||
| K-03 | **Analisis Kesenjangan (Gap Analysis)** — Identifikasi wilayah miskin yang kurang terjangkau rumah ibadah | ✅ Selesai |
|
||||
| K-04 | **Ekspor Laporan Terfilter** — Ekspor khusus per radius/wilayah untuk laporan resmi | ✅ Selesai |
|
||||
|
||||
---
|
||||
|
||||
## 🖨️ 5. Fitur Cetak & Ekspor Lanjutan
|
||||
|
||||
| Fitur | Role terkait | Status |
|
||||
|---|---|---|
|
||||
| **Cetak Laporan Wilayah** — Print daftar keluarga miskin dalam radius rumah ibadah | I-04 | ✅ Selesai |
|
||||
| **Ekspor PDF** — Format laporan resmi | K-04 | ✅ Selesai |
|
||||
| **Ekspor per Radius** — Data yang terfilter per analisis radius | I-04, K-04 | ✅ Selesai |
|
||||
|
||||
---
|
||||
|
||||
## 🎓 6. Saran Fitur dari Dosen (Di Luar SKPL)
|
||||
|
||||
Fitur-fitur berikut diusulkan setelah SKPL final dan belum ada dalam dokumen spesifikasi awal.
|
||||
|
||||
### 6.1 Data Anggota Keluarga (Tabel Baru: `anggota_keluarga`)
|
||||
|
||||
Saat ini `rumah_miskin` hanya menyimpan jumlah KK dan jumlah jiwa secara agregat. Fitur ini menambahkan data **per-orang** di dalam rumah tangga.
|
||||
|
||||
| Sub-fitur | Keterangan | Status |
|
||||
|---|---|---|
|
||||
| **Data Umur** | Simpan tanggal lahir tiap anggota, hitung umur otomatis | ❌ Belum |
|
||||
| **Riwayat Pendidikan** | Tingkat pendidikan tiap anggota (tidak sekolah / SD / SMP / SMA / D3 / S1+) | ❌ Belum |
|
||||
| **Status Hidup** | Status `hidup` / `meninggal` per-anggota, bisa diupdate petugas | ❌ Belum |
|
||||
|
||||
**Skema yang disarankan (`anggota_keluarga`):**
|
||||
|
||||
| Kolom | Tipe | Keterangan |
|
||||
|---|---|---|
|
||||
| `id` | BIGINT PK | |
|
||||
| `id_rumah` | FK → rumah_miskin | |
|
||||
| `nama` | VARCHAR | |
|
||||
| `hubungan` | ENUM | kepala_keluarga, istri, anak, lainnya |
|
||||
| `tanggal_lahir` | DATE | untuk hitung umur |
|
||||
| `pendidikan` | ENUM | tidak_sekolah, sd, smp, sma, d3, s1_plus |
|
||||
| `status_hidup` | ENUM | hidup, meninggal |
|
||||
| timestamps | | |
|
||||
|
||||
**Dampak implementasi:** Perlu form tambah/edit anggota di halaman `/data` (nested di dalam detail rumah miskin), dan statistik umur di dashboard.
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Riwayat Bantuan (Tabel Baru: `riwayat_bantuan`)
|
||||
|
||||
Mencatat setiap bantuan yang telah diberikan rumah ibadah kepada keluarga miskin — membentuk jejak audit distribusi bantuan.
|
||||
|
||||
| Kolom | Tipe | Keterangan |
|
||||
|---|---|---|
|
||||
| `id` | BIGINT PK | |
|
||||
| `id_rumah` | FK → rumah_miskin | penerima bantuan |
|
||||
| `ibadah_id` | FK → rumah_ibadah nullable | pemberi bantuan |
|
||||
| `jenis_bantuan` | VARCHAR | contoh: sembako, uang, pakaian |
|
||||
| `keterangan` | TEXT | deskripsi bebas |
|
||||
| `tanggal_bantuan` | DATE | |
|
||||
| `dicatat_oleh` | FK → users | |
|
||||
| timestamps | | |
|
||||
|
||||
**Dampak implementasi:** Tombol "Catat Bantuan" di popup marker / tabel data, halaman riwayat per-keluarga, dan statistik total bantuan di dashboard.
|
||||
|
||||
**Status:** ❌ Belum
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Permohonan Transfer Antar Rumah Ibadah (Tabel Baru: `permohonan_transfer`)
|
||||
|
||||
Ketika kapasitas jangkauan rumah ibadah sudah penuh, pengurus dapat mengajukan permohonan agar sebagian keluarga miskin dialihkan ke rumah ibadah lain yang masih mencukupi.
|
||||
|
||||
**Alur:**
|
||||
1. Pengurus Ibadah A melihat analisis radius → kapasitas melebihi batas
|
||||
2. Pengurus mengajukan permohonan transfer → pilih keluarga miskin + pilih rumah ibadah tujuan
|
||||
3. Pengurus Ibadah B menerima notifikasi → menyetujui atau menolak
|
||||
4. Administrator bisa memantau semua permohonan
|
||||
|
||||
| Kolom | Tipe | Keterangan |
|
||||
|---|---|---|
|
||||
| `id` | BIGINT PK | |
|
||||
| `dari_ibadah_id` | FK → rumah_ibadah | pengaju |
|
||||
| `ke_ibadah_id` | FK → rumah_ibadah | tujuan |
|
||||
| `id_rumah` | FK → rumah_miskin | keluarga yang diajukan |
|
||||
| `alasan` | TEXT | |
|
||||
| `status` | ENUM | pending, disetujui, ditolak |
|
||||
| `catatan_respons` | TEXT nullable | dari pengurus penerima |
|
||||
| `diajukan_oleh` | FK → users | |
|
||||
| `diproses_oleh` | FK → users nullable | |
|
||||
| timestamps | | |
|
||||
|
||||
**Dampak implementasi:** Halaman `/permohonan` (list + form submit + approve/reject), badge notifikasi di navbar pengurus, dan log aktivitas untuk setiap perubahan status.
|
||||
|
||||
**Status:** ❌ Belum
|
||||
|
||||
---
|
||||
|
||||
### Estimasi Kompleksitas
|
||||
|
||||
| Fitur | Kompleksitas | Estimasi |
|
||||
|---|---|---|
|
||||
| Data anggota keluarga (umur + pendidikan + status hidup) | Tinggi | 1 plan penuh |
|
||||
| Riwayat bantuan | Menengah | 1 plan penuh |
|
||||
| Permohonan transfer antar rumah ibadah | Sangat Tinggi | 1–2 plan |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Ringkasan
|
||||
|
||||
| Kategori | Selesai | Belum |
|
||||
|---|---|---|
|
||||
| Autentikasi & RBAC | ✅ 6/6 komponen | — |
|
||||
| Role Pengguna | ✅ 4/4 role | — |
|
||||
| Ownership pengurus_ibadah | ✅ 1/1 | — |
|
||||
| Fitur Admin | ✅ 8/8 | — |
|
||||
| Fitur Edit Data | ✅ 2/2 | — |
|
||||
| Fitur Analisis/Dashboard | ✅ 3/3 | — |
|
||||
| Fitur Cetak/Ekspor | ✅ 3/3 | — |
|
||||
| Fitur Saran Dosen | — | ❌ 3 fitur baru (di luar SKPL) |
|
||||
| **Total sisa (SKPL)** | | **0 — Semua fitur SKPL selesai ✅** |
|
||||
| **Total saran dosen** | | **~3 plan** |
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Urutan Plan yang Disarankan
|
||||
|
||||
| Plan | Isi | Prioritas |
|
||||
|---|---|---|
|
||||
| ~~Plan 1~~ | ~~Auth & RBAC~~ | ✅ Selesai |
|
||||
| ~~Plan 2~~ | ~~Form Edit Rumah Ibadah & Keluarga Miskin + Ownership~~ | ✅ Selesai |
|
||||
| ~~Plan 3~~ | ~~Manajemen Pengguna (Admin: CRUD user, assign role)~~ | ✅ Selesai |
|
||||
| ~~Plan 4~~ | ~~Dashboard Statistik + Gap Analysis (Pemangku Kebijakan)~~ | ✅ Selesai |
|
||||
| ~~Plan 5~~ | ~~Ekspor PDF + Ekspor per Radius + Cetak Laporan~~ | ✅ Selesai |
|
||||
| ~~Plan 6~~ | ~~Fitur Admin lanjutan (Bulk delete, CSV import, Log, Backup)~~ | ✅ Selesai |
|
||||
| ~~Plan 7~~ | ~~Statistik, Backup Terjadwal, Notifikasi, Restore~~ | ✅ Selesai |
|
||||
| ~~A-03~~ | ~~Konfigurasi Sistem (center peta, zoom, radius min/max)~~ | ✅ Selesai |
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
@@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\RumahIbadah;
|
||||
use App\Models\RumahMiskin;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAdvancedTest 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;
|
||||
}
|
||||
|
||||
// ── ACTIVITY LOG ─────────────────────────────────────────────
|
||||
public function test_creating_ibadah_writes_activity_log(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->postJson('/api/ibadah', ['nama' => 'Masjid Log', 'latitude' => -7, 'longitude' => 110])->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('activity_logs', ['action' => 'create', 'subject_type' => 'ibadah', 'user_id' => $admin->id]);
|
||||
}
|
||||
|
||||
public function test_admin_can_list_logs(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
ActivityLog::record('login', 'Test masuk');
|
||||
|
||||
$this->getJson('/api/admin/logs')->assertOk()->assertJsonFragment(['description' => 'Test masuk']);
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_list_logs(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('petugas_pendataan'));
|
||||
$this->getJson('/api/admin/logs')->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_log_page_accessible_by_admin_only(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
$this->get('/admin/logs')->assertOk();
|
||||
|
||||
$this->actingAs($this->createUser('pengurus_ibadah'));
|
||||
$this->get('/admin/logs')->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── BULK DELETE ──────────────────────────────────────────────
|
||||
public function test_admin_can_bulk_delete_ibadah(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$a = RumahIbadah::create(['user_id' => $admin->id, 'nama' => 'A', 'latitude' => -7, 'longitude' => 110, 'radius' => 500]);
|
||||
$b = RumahIbadah::create(['user_id' => $admin->id, 'nama' => 'B', 'latitude' => -7, 'longitude' => 110, 'radius' => 500]);
|
||||
|
||||
$this->postJson('/api/admin/ibadah/bulk-delete', ['ids' => [$a->id, $b->id]])
|
||||
->assertOk()->assertJsonFragment(['deleted' => 2]);
|
||||
|
||||
$this->assertDatabaseCount('rumah_ibadah', 0);
|
||||
}
|
||||
|
||||
public function test_admin_can_bulk_delete_miskin(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
|
||||
RumahMiskin::create(['id_rumah' => 'RM1', 'latitude' => -7, 'longitude' => 110, 'jumlah_kk' => 1, 'jumlah_orang' => 1]);
|
||||
RumahMiskin::create(['id_rumah' => 'RM2', 'latitude' => -7, 'longitude' => 110, 'jumlah_kk' => 1, 'jumlah_orang' => 1]);
|
||||
|
||||
$this->postJson('/api/admin/miskin/bulk-delete', ['ids' => ['RM1', 'RM2']])
|
||||
->assertOk()->assertJsonFragment(['deleted' => 2]);
|
||||
|
||||
$this->assertDatabaseCount('rumah_miskin', 0);
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_bulk_delete(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('petugas_pendataan'));
|
||||
$this->postJson('/api/admin/miskin/bulk-delete', ['ids' => ['RM1']])->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── CSV IMPORT ───────────────────────────────────────────────
|
||||
public function test_admin_can_import_miskin_csv(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
|
||||
$csv = "ID Rumah,Nama KK,Alamat,KK,Jiwa,Pekerjaan,Pendapatan,Kondisi,Lat,Lon\n"
|
||||
. "IMP1,Budi,Jl Test,2,5,Buruh,1000000,buruk,-7.0,110.0\n";
|
||||
$file = UploadedFile::fake()->createWithContent('miskin.csv', $csv);
|
||||
|
||||
$this->post('/api/admin/import/miskin', ['file' => $file])
|
||||
->assertOk()->assertJsonFragment(['imported' => 1, 'failed' => 0]);
|
||||
|
||||
$this->assertDatabaseHas('rumah_miskin', ['id_rumah' => 'IMP1', 'nama_kk' => 'Budi']);
|
||||
}
|
||||
|
||||
public function test_admin_can_import_ibadah_csv(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
|
||||
$csv = "ID,Nama,Jenis,Alamat,Radius,Jemaah,Pengurus,Telepon,Lat,Lon\n"
|
||||
. "1,Masjid Import,masjid,Jl Test,600,100,Pak Imam,0812,-7.0,110.0\n";
|
||||
$file = UploadedFile::fake()->createWithContent('ibadah.csv', $csv);
|
||||
|
||||
$this->post('/api/admin/import/ibadah', ['file' => $file])
|
||||
->assertOk()->assertJsonFragment(['imported' => 1, 'failed' => 0]);
|
||||
|
||||
$this->assertDatabaseHas('rumah_ibadah', ['nama' => 'Masjid Import', 'radius' => 600]);
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_import(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('petugas_pendataan'));
|
||||
$file = UploadedFile::fake()->createWithContent('x.csv', "a\n");
|
||||
$this->post('/api/admin/import/miskin', ['file' => $file])->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── BACKUP ───────────────────────────────────────────────────
|
||||
public function test_unauthenticated_cannot_backup(): void
|
||||
{
|
||||
$this->get('/api/admin/backup')->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_backup(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('pengurus_ibadah'));
|
||||
$this->get('/api/admin/backup')->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_admin_can_download_backup(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
$response = $this->get('/api/admin/backup');
|
||||
$response->assertOk();
|
||||
$response->assertHeader('content-type', 'application/sql');
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminUserTest 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;
|
||||
}
|
||||
|
||||
public function test_admin_users_page_is_accessible(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->get('/admin/users')->assertOk();
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_access_users_page(): void
|
||||
{
|
||||
$pengurus = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($pengurus);
|
||||
|
||||
$this->get('/admin/users')->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_admin_can_list_users(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->getJson('/api/admin/users')
|
||||
->assertOk()
|
||||
->assertJsonFragment(['email' => $admin->email]);
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_list_users(): void
|
||||
{
|
||||
$pengurus = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($pengurus);
|
||||
|
||||
$this->getJson('/api/admin/users')->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_admin_can_create_user(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->postJson('/api/admin/users', [
|
||||
'name' => 'User Baru',
|
||||
'email' => 'userbaru@webgis.local',
|
||||
'password' => 'password123',
|
||||
'role' => 'petugas_pendataan',
|
||||
])->assertOk()->assertJsonFragment(['email' => 'userbaru@webgis.local']);
|
||||
|
||||
$this->assertDatabaseHas('users', ['email' => 'userbaru@webgis.local']);
|
||||
}
|
||||
|
||||
public function test_admin_can_update_user(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$target = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->putJson("/api/admin/users/{$target->id}", [
|
||||
'name' => 'Nama Diubah',
|
||||
'email' => $target->email,
|
||||
'role' => 'petugas_pendataan',
|
||||
])->assertOk()->assertJsonFragment(['name' => 'Nama Diubah']);
|
||||
|
||||
$this->assertTrue($target->fresh()->hasRole('petugas_pendataan'));
|
||||
}
|
||||
|
||||
public function test_admin_can_delete_other_user(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$target = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->deleteJson("/api/admin/users/{$target->id}")->assertOk();
|
||||
$this->assertNull(User::find($target->id));
|
||||
}
|
||||
|
||||
public function test_admin_cannot_delete_self(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->deleteJson("/api/admin/users/{$admin->id}")->assertStatus(403);
|
||||
$this->assertNotNull(User::find($admin->id));
|
||||
}
|
||||
|
||||
public function test_admin_can_reset_password(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$target = $this->createUser('pengurus_ibadah');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$this->putJson("/api/admin/users/{$target->id}/password", [
|
||||
'password' => 'newpassword123',
|
||||
'password_confirmation' => 'newpassword123',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertTrue(
|
||||
\Illuminate\Support\Facades\Hash::check('newpassword123', $target->fresh()->password)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
public function test_users_table_exists_and_user_can_be_created(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => 'Test Admin',
|
||||
'email' => 'admin@test.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', ['email' => 'admin@test.com']);
|
||||
}
|
||||
|
||||
public function test_roles_seeder_creates_four_roles(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
|
||||
$this->assertDatabaseHas('roles', ['name' => 'administrator']);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'pengurus_ibadah']);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'petugas_pendataan']);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'pemangku_kebijakan']);
|
||||
}
|
||||
|
||||
public function test_roles_seeder_creates_default_admin(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
|
||||
$user = User::where('email', 'admin@webgis.local')->first();
|
||||
$this->assertNotNull($user);
|
||||
$this->assertTrue($user->hasRole('administrator'));
|
||||
}
|
||||
|
||||
public function test_login_page_is_accessible(): void
|
||||
{
|
||||
$response = $this->get('/login');
|
||||
$response->assertStatus(200);
|
||||
$response->assertViewIs('auth.login');
|
||||
}
|
||||
|
||||
public function test_login_with_valid_credentials_redirects_to_map(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => 'admin@webgis.local',
|
||||
'password' => 'admin123',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/map');
|
||||
}
|
||||
|
||||
public function test_login_with_invalid_credentials_returns_error(): void
|
||||
{
|
||||
$response = $this->post('/login', [
|
||||
'email' => 'wrong@email.com',
|
||||
'password' => 'wrongpass',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
}
|
||||
|
||||
public function test_logout_redirects_to_login(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
$user = User::where('email', 'admin@webgis.local')->first();
|
||||
|
||||
$response = $this->actingAs($user)->post('/logout');
|
||||
|
||||
$response->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function test_unauthenticated_user_cannot_access_map(): void
|
||||
{
|
||||
$response = $this->get('/map');
|
||||
$response->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function test_pemangku_kebijakan_cannot_delete_ibadah(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
|
||||
$viewer = User::firstOrCreate(
|
||||
['email' => 'viewer@test.com'],
|
||||
['name' => 'Viewer', 'password' => bcrypt('password')]
|
||||
);
|
||||
$viewer->syncRoles(['pemangku_kebijakan']);
|
||||
|
||||
$response = $this->actingAs($viewer)->delete('/api/ibadah/999');
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_data_page_shows_user_name_and_logout(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
$user = User::where('email', 'admin@webgis.local')->first();
|
||||
|
||||
$response = $this->actingAs($user)->get('/data');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Keluar');
|
||||
$response->assertSee($user->name);
|
||||
}
|
||||
|
||||
public function test_map_page_passes_user_role_to_js(): void
|
||||
{
|
||||
$this->artisan('db:seed', ['--class' => 'RolesAndAdminSeeder']);
|
||||
$user = User::where('email', 'admin@webgis.local')->first();
|
||||
|
||||
$response = $this->actingAs($user)->get('/map');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee("authUserRole = 'administrator'", false);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DashboardTest 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;
|
||||
}
|
||||
|
||||
public function test_unauthenticated_cannot_access_dashboard(): void
|
||||
{
|
||||
$this->get('/dashboard')->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function test_administrator_can_access_dashboard(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
$this->get('/dashboard')->assertOk();
|
||||
}
|
||||
|
||||
public function test_pengurus_ibadah_can_access_dashboard(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('pengurus_ibadah'));
|
||||
$this->get('/dashboard')->assertOk();
|
||||
}
|
||||
|
||||
public function test_petugas_pendataan_can_access_dashboard(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('petugas_pendataan'));
|
||||
$this->get('/dashboard')->assertOk();
|
||||
}
|
||||
|
||||
public function test_pemangku_kebijakan_can_access_dashboard(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('pemangku_kebijakan'));
|
||||
$this->get('/dashboard')->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_unauthenticated_root_redirects_to_login(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertRedirect('/login');
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\RumahIbadah;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExportTest 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;
|
||||
}
|
||||
|
||||
public function test_unauthenticated_cannot_export_pdf(): void
|
||||
{
|
||||
$this->get('/export/pdf')->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function test_authenticated_can_export_all_miskin_pdf(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('pemangku_kebijakan'));
|
||||
|
||||
$response = $this->get('/export/pdf');
|
||||
$response->assertOk();
|
||||
$response->assertHeader('content-type', 'application/pdf');
|
||||
}
|
||||
|
||||
public function test_radius_pdf_returns_404_for_invalid_ibadah(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
|
||||
$this->get('/export/pdf/radius/99999/500')->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_radius_pdf_returns_pdf_for_valid_ibadah(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
$ibadah = RumahIbadah::create([
|
||||
'user_id' => $admin->id,
|
||||
'nama' => 'Test Masjid',
|
||||
'jenis' => 'masjid',
|
||||
'latitude' => -7.0,
|
||||
'longitude' => 110.0,
|
||||
'radius' => 500,
|
||||
]);
|
||||
|
||||
$response = $this->get("/export/pdf/radius/{$ibadah->id}/500");
|
||||
$response->assertOk();
|
||||
$response->assertHeader('content-type', 'application/pdf');
|
||||
}
|
||||
|
||||
public function test_radius_pdf_rejects_non_numeric_params(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
|
||||
$this->get('/export/pdf/radius/abc/def')->assertNotFound();
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\RumahIbadah;
|
||||
use App\Models\RumahMiskin;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\TestCase;
|
||||
|
||||
class Plan7Test 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;
|
||||
}
|
||||
|
||||
// ── STATISTIK ────────────────────────────────────────────────
|
||||
public function test_admin_can_get_stats(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
ActivityLog::record('login', 'Test');
|
||||
|
||||
$res = $this->getJson('/api/admin/stats')->assertOk();
|
||||
$res->assertJsonStructure(['logins_today', 'logins_week', 'logins_month', 'actions_today', 'by_type', 'top_users', 'daily']);
|
||||
$this->assertGreaterThanOrEqual(1, $res->json('actions_today'));
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_get_stats(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('petugas_pendataan'));
|
||||
$this->getJson('/api/admin/stats')->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── NOTIFIKASI ────────────────────────────────────────────────
|
||||
public function test_authenticated_user_can_poll_notifications(): void
|
||||
{
|
||||
$admin = $this->createUser('administrator');
|
||||
$this->actingAs($admin);
|
||||
|
||||
RumahIbadah::create(['user_id' => $admin->id, 'nama' => 'Test', 'latitude' => -7, 'longitude' => 110, 'radius' => 500]);
|
||||
|
||||
$since = now()->subMinutes(5)->toISOString();
|
||||
$res = $this->getJson('/api/notifications?since=' . urlencode($since))->assertOk();
|
||||
$res->assertJsonStructure(['new_ibadah', 'new_miskin', 'latest_ibadah', 'latest_miskin']);
|
||||
$this->assertEquals(1, $res->json('new_ibadah'));
|
||||
}
|
||||
|
||||
public function test_unauthenticated_cannot_poll_notifications(): void
|
||||
{
|
||||
$this->get('/api/notifications')->assertRedirect('/login');
|
||||
}
|
||||
|
||||
// ── BACKUP LIST ───────────────────────────────────────────────
|
||||
public function test_admin_can_list_backups(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
$this->getJson('/api/admin/backup/list')->assertOk()->assertJsonIsArray();
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_list_backups(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('pengurus_ibadah'));
|
||||
$this->getJson('/api/admin/backup/list')->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── RESTORE ───────────────────────────────────────────────────
|
||||
public function test_restore_requires_confirm_word(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('administrator'));
|
||||
$file = UploadedFile::fake()->createWithContent('backup.sql', '-- test sql');
|
||||
$this->post('/api/admin/restore', ['file' => $file, 'confirm' => 'WRONG'])->assertStatus(302);
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_restore(): void
|
||||
{
|
||||
$this->actingAs($this->createUser('petugas_pendataan'));
|
||||
$file = UploadedFile::fake()->createWithContent('backup.sql', '-- test');
|
||||
$this->post('/api/admin/restore', ['file' => $file, 'confirm' => 'RESTORE'])->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_unauthenticated_cannot_restore(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent('backup.sql', '-- test');
|
||||
$this->post('/api/admin/restore', ['file' => $file, 'confirm' => 'RESTORE'])->assertRedirect('/login');
|
||||
}
|
||||
|
||||
// ── BACKUP COMMAND ────────────────────────────────────────────
|
||||
public function test_backup_database_command_runs_successfully(): void
|
||||
{
|
||||
$this->artisan('backup:database')->assertSuccessful();
|
||||
|
||||
$files = glob(storage_path('app/backups/backup-webgis-*.sql'));
|
||||
$this->assertNotEmpty($files);
|
||||
foreach ($files as $f) unlink($f);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user