Tutorial Laravel: Membuat Sistem Pengaduan Masyarakat (Tahap 6: CRUD Pengaduan User & Upload Foto Bukti)

Setelah halaman dashboard dan sistem keamanan multi-role selesai, saatnya kita masuk ke fitur utama aplikasi dari sisi pengguna (Masyarakat). Pada Tahap 6 ini, kita akan membuat fitur pengajuan laporan pengaduan lengkap dengan unggah foto bukti serta tabel riwayat laporan pengguna.
📋 Ringkasan Tahap 6
- Tujuan: Menyiapkan pengiriman laporan pengaduan, sistem penyimpanan file foto ke storage, dan menampilkan riwayat laporan milik user.
- Fitur Utama: Form pengaduan (
multipart/form-data), konversi link direktori storage public, dan relasi data riwayat pengaduan. - Estimasi Waktu: 6 – 8 Menit.
🛠️ Langkah-Langkah Eksekusi
1. Menghubungkan Storage Laravel ke Publik
Agar foto bukti yang diunggah pengguna dapat diakses dan ditampilkan di browser, kita perlu membuat symbolic link dari direktori penyimpanan internal Laravel ke folder publik.
Jalankan perintah ini di terminal:
php artisan storage:link
Penjelasan: Perintah ini membuat folder shortcut
storagedi dalam folderpublic/, sehingga file media bisa diakses melalui URL aplikasi secara aman.
2. Membuat ComplaintController
Selanjutnya, kita buat controller yang bertanggung jawab menangani proses validasi data dan penyimpanan berkas unggahan.
Jalankan perintah Artisan berikut:
php artisan make:controller ComplaintController
Buka file app/Http/Controllers/ComplaintController.php dan isi kodenya:
<?php
namespace App\Http\Controllers;
use App\Models\Complaint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ComplaintController extends Controller
{
// Memproses dan menyimpan data pengaduan baru
public function store(Request $request)
{
// 1. Validasi Input Data
$request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', // Maksimal 2MB
]);
// 2. Handling File Upload Foto
$imagePath = null;
if ($request->hasFile('image')) {
$imagePath = $request->file('image')->store('complaints', 'public');
}
// 3. Simpan ke Database
Complaint::create([
'user_id' => Auth::id(),
'title' => $request->title,
'description' => $request->description,
'image' => $imagePath,
'status' => 'pending',
]);
return back()->with('success', 'Laporan pengaduan Anda berhasil dikirim!');
}
}
3. memperbarui Route User (routes/web.php)
Buka file routes/web.php dan perbarui bagian kelompok route User agar mengirimkan data riwayat pengaduan serta menyediakan endpoint penampung data form:
use App\Http\Controllers\ComplaintController;
use App\Models\Complaint;
// Kelompok Route khusus User (Masyarakat)
Route::middleware(['auth', 'role:user'])->prefix('user')->group(function () {
Route::get('/dashboard', function () {
// Mengambil daftar pengaduan milik user yang sedang login saja
$complaints = Complaint::where('user_id', Auth::id())->latest()->get();
return view('user.dashboard', compact('complaints'));
})->name('user.dashboard');
// Route penampung form submit pengaduan
Route::post('/complaints', [ComplaintController::class, 'store'])->name('complaints.store');
});
4. Memperbarui Tampilan Dashboard User (user/dashboard.blade.php)
Buka file resources/views/user/dashboard.blade.php dan ganti isinya dengan antarmuka Form Pengajuan Pengaduan beserta Tabel Riwayat Laporan:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Masyarakat - Layanan Pengaduan</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<!-- Navbar Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="#">Layanan Pengaduan</a>
<div class="d-flex align-items-center text-white">
<span class="me-3">Halo, <strong>{{ Auth::user()->name }}</strong></span>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit" class="btn btn-warning btn-sm">Logout</button>
</form>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container mt-4 mb-5">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<div class="row">
<!-- Col 1: Form Buat Pengaduan Baru -->
<div class="col-md-5 mb-4">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Buat Laporan Pengaduan</h5>
</div>
<div class="card-body">
<form action="{{ route('complaints.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label class="form-label">Judul Laporan</label>
<input type="text" name="title" class="form-control" placeholder="Contoh: Jalan Rusak di RT 02" required>
</div>
<div class="mb-3">
<label class="form-label">Isi / Detail Pengaduan</label>
<textarea name="description" class="form-control" rows="4" placeholder="Jelaskan detail kejadian secara rinci..." required></textarea>
</div>
<div class="mb-3">
<label class="form-label">Foto Bukti (Opsional)</label>
<input type="file" name="image" class="form-control" accept="image/*">
<small class="text-muted">Format: JPG, JPEG, PNG (Maks 2MB)</small>
</div>
<button type="submit" class="btn btn-primary w-100">Kirim Laporan Pengaduan</button>
</form>
</div>
</div>
</div>
<!-- Col 2: Tabel Riwayat Pengaduan Saya -->
<div class="col-md-7">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Riwayat Laporan Anda</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th>Judul</th>
<th>Status</th>
<th>Foto</th>
<th>Tanggal</th>
</tr>
</thead>
<tbody>
@forelse($complaints as $complaint)
<tr>
<td class="fw-bold">{{ $complaint->title }}</td>
<td>
@if($complaint->status == 'pending')
<span class="badge bg-warning text-dark">Pending</span>
@elseif($complaint->status == 'processing')
<span class="badge bg-info">Diproses</span>
@elseif($complaint->status == 'resolved')
<span class="badge bg-success">Selesai</span>
@else
<span class="badge bg-danger">Ditolak</span>
@endif
</td>
<td>
@if($complaint->image)
<a href="{{ asset('storage/' . $complaint->image) }}" target="_blank" class="btn btn-outline-secondary btn-sm">Lihat Foto</a>
@else
<span class="text-muted">-</span>
@endif
</td>
<td>{{ $complaint->created_at->format('d/m/Y') }}</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center py-4 text-muted">Belum ada laporan pengaduan yang dibuat.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
