Rumah Dokumentasi
Dokumentasi lengkap proyek Sistem Pelaporan UKK berbasis Laravel 12 + Breeze Blade. Tersedia penjelasan setiap controller, model, migrasi, middleware, hingga tampilan Blade.
Aplikasi web untuk mencatat dan mengelola laporan dari siswa. Siswa dapat membuat laporan beserta foto, sedangkan admin mengelola kategori dan memberikan tanggapan/status pada setiap laporan yang masuk.
Peran Pengguna
Stack Teknologi
- Framework: Laravel 12 (PHP 8.2+)
- Auth Starter: Laravel Breeze — Blade stack
- Frontend: Blade Templates + Tailwind CSS v3 (via Vite)
- Database: MySQL 8
- Build Tool: Vite + PostCSS
Alur Siswa
Alur Admin
Status Laporan
pendingdiprosesselesaiditolak
# Clone & install git clone <repo-url> && cd UKK composer install npm install # Setup environment cp .env.example .env php artisan key:generate # Setup database (isi .env dulu) php artisan migrate --seed # Build assets & jalankan npm run build php artisan serve
bootstrap/app.php baru untuk registrasi middleware. Tidak ada lagi Kernel.php — middleware didaftarkan langsung di app.php.
Struktur Folder
Organisasi lengkap file dan direktori pada proyek ini.
app/ ├── Http/ │ ├── Controllers/ │ │ ├── Auth/ ← 7 controller bawaan Breeze │ │ ├── Controller.php ← Abstract base controller │ │ ├── KategoriController.php ← CRUD kategori (Admin) │ │ ├── LaporanController.php ← CRUD laporan + feedback │ │ ├── ProfileController.php ← Kelola profil user │ │ └── TanggapanController.php ← Komentar pada laporan │ ├── Middleware/ │ │ └── RoleMiddleware.php ← Cek role admin/siswa │ └── Requests/ │ ├── StoreKategoriRequest.php │ ├── StoreLaporanRequest.php │ ├── StoreTanggapanRequest.php │ ├── UpdateKategoriRequest.php │ ├── UpdateLaporanRequest.php │ ├── UpdateTanggapanRequest.php │ └── ProfileUpdateRequest.php ├── Models/ │ ├── Kategori.php │ ├── Laporan.php │ ├── Tanggapan.php │ └── User.php ├── Policies/ │ ├── KategoriPolicy.php │ ├── LaporanPolicy.php │ └── TanggapanPolicy.php └── Providers/ └── AppServiceProvider.php
resources/views/ ├── Admin/ │ ├── dashboard.blade.php │ ├── Kategori/ ← create, edit, index │ ├── Laporan/ ← tanggapi │ └── Tanggapan/ ← index ├── Siswa/ │ ├── dashboard.blade.php │ └── Laporan/ ← create, edit, index ├── auth/ ← login, register, reset-password, dll ├── layouts/ ← app, guest, navigation ├── components/ ← reusable blade components └── profile/ ← edit + partials database/ ├── migrations/ ← 4 migration files ├── factories/ ← Kategori, Laporan, Tanggapan, User └── seeders/ ← DatabaseSeeder + per-model seeders
bootstrap/ ├── app.php ← Middleware & routing didaftarkan di sini └── providers.php
app/Http/Kernel.php. Middleware global dan route middleware didaftarkan langsung di bootstrap/app.php menggunakan fluent API.
Database & Migrasi
Empat tabel utama yang digunakan aplikasi beserta relasinya.
Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->enum('role', ['admin', 'siswa'])->default('siswa'); $table->rememberToken(); $table->timestamps(); });
Schema::create('kategoris', function (Blueprint $table) { $table->id(); $table->string('nama'); $table->text('deskripsi')->nullable(); $table->timestamps(); });
status sekaligus (enum + string). Versi di bawah sudah diperbaiki — hanya gunakan satu kolom enum.
Schema::create('laporans', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->foreignId('kategori_id')->constrained()->cascadeOnDelete(); $table->foreignId('admin_id')->nullable()->constrained('users')->nullOnDelete(); $table->string('lokasi'); $table->text('deskripsi'); $table->string('foto')->nullable(); // ✅ Satu kolom status saja (enum) $table->enum('status', ['pending', 'diproses', 'selesai', 'ditolak']) ->default('pending'); $table->text('admin_feedback')->nullable(); $table->timestamp('diterima_pada')->nullable(); $table->timestamp('ditanggapi_pada')->nullable(); $table->timestamp('selesai_pada')->nullable(); $table->timestamps(); });
Schema::create('tanggapans', function (Blueprint $table) { $table->id(); $table->foreignId('laporan_id')->constrained()->cascadeOnDelete(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->text('isi'); $table->timestamps(); });
| Dari Tabel | Relasi | Ke Tabel | Keterangan |
|---|---|---|---|
laporans | belongsTo | users | Pembuat laporan (siswa) |
laporans | belongsTo | kategoris | Kategori laporan |
laporans | belongsTo | users (admin) | Admin yang menangani |
laporans | hasMany | tanggapans | Komentar pada laporan |
tanggapans | belongsTo | laporans | Laporan yang dikomentari |
tanggapans | belongsTo | users | Pemberi komentar |
users | hasMany | laporans | Semua laporan milik user |
Routes
Peta semua URL di routes/web.php, dikelompokkan berdasarkan akses pengguna.
| Method | URL | Fungsi |
|---|---|---|
| GET | / | Halaman welcome |
| GET | /login | Form login |
| POST | /login | Proses login |
| GET | /register | Form registrasi |
| POST | /register | Proses registrasi |
| GET | /forgot-password | Form lupa password |
| POST | /forgot-password | Kirim link reset |
| GET | /reset-password/{token} | Form reset password |
| POST | /reset-password | Proses reset password |
Route::middleware(['auth', 'role:admin'])->prefix('admin')->group(function () { Route::get('/dashboard', ...)->name('admin.dashboard'); // Kategori — full resource Route::resource('kategori', KategoriController::class); // Feedback laporan Route::post('/laporan/{laporan}/feedback', [LaporanController::class, 'feedback']) ->name('admin.laporan.feedback'); // Tanggapan Route::resource('tanggapan', TanggapanController::class); });
| Method | URL | Controller@Method |
|---|---|---|
| GET | /admin/dashboard | dashboard admin |
| GET | /admin/kategori | KategoriController@index |
| GET | /admin/kategori/create | KategoriController@create |
| POST | /admin/kategori | KategoriController@store |
| GET | /admin/kategori/{id}/edit | KategoriController@edit |
| PUT | /admin/kategori/{id} | KategoriController@update |
| DELETE | /admin/kategori/{id} | KategoriController@destroy |
| POST | /admin/laporan/{id}/feedback | LaporanController@feedback |
| GET | /admin/tanggapan | TanggapanController@index |
| Method | URL | Controller@Method |
|---|---|---|
| GET | /siswa/dashboard | dashboard siswa |
| GET | /laporan | LaporanController@index |
| GET | /laporan/create | LaporanController@create |
| POST | /laporan | LaporanController@store |
| GET | /laporan/{id}/edit | LaporanController@edit |
| PUT | /laporan/{id} | LaporanController@update |
| DELETE | /laporan/{id} | LaporanController@destroy |
| Method | URL | Controller@Method |
|---|---|---|
| GET | /profile | ProfileController@edit |
| PUT | /profile | ProfileController@update |
| DELETE | /profile | ProfileController@destroy |
| POST | /logout | AuthenticatedSessionController@destroy |
LaporanController
Controller utama untuk mengelola data laporan. Diakses oleh Admin (feedback) dan Siswa (CRUD laporan sendiri).
- Path:
app/Http/Controllers/LaporanController.php - Namespace:
App\Http\Controllers - Middleware:
auth+role:siswa(kecuali feedback:role:admin) - Form Requests:
StoreLaporanRequest,UpdateLaporanRequest - Model:
Laporan,Kategori
user_id agar siswa hanya melihat laporan miliknya sendiri.public function index() { $laporans = Laporan::where('user_id', auth()->id()) ->with(['kategori']) ->latest() ->get(); return view('Siswa.Laporan.index', compact('laporans')); }
public function create() { $kategoris = Kategori::all(); return view('Siswa.Laporan.create', compact('kategoris')); }
storage/app/public/laporan_fotos.public function store(StoreLaporanRequest $request) { $data = $request->validated(); $data['user_id'] = auth()->id(); if ($request->hasFile('foto')) { $data['foto'] = $request->file('foto') ->store('laporan_fotos', 'public'); } Laporan::create($data); return redirect()->route('laporan.index') ->with('success', 'Laporan berhasil dikirim'); }
pending.public function edit(Laporan $laporan) { $kategoris = Kategori::all(); return view('Siswa.Laporan.edit', compact('laporan', 'kategoris')); }
public function update(UpdateLaporanRequest $request, Laporan $laporan) { $data = $request->validated(); if ($request->hasFile('foto')) { if ($laporan->foto) { Storage::disk('public')->delete($laporan->foto); } $data['foto'] = $request->file('foto') ->store('laporan_fotos', 'public'); } $laporan->update($data); return redirect()->route('laporan.index') ->with('success', 'Laporan berhasil diperbarui'); }
public function destroy(Laporan $laporan) { if ($laporan->foto) { Storage::disk('public')->delete($laporan->foto); } $laporan->delete(); return back()->with('success', 'Laporan berhasil dihapus'); }
ditolak namun feedback kosong, akan diisi teks default otomatis.public function feedback(Request $request, Laporan $laporan) { // Update status & feedback dari form admin $laporan->status = $request->status; $laporan->admin_feedback = $request->admin_feedback; $laporan->ditanggapi_pada = now(); // Jika ditolak tapi alasan kosong → isi default if ($request->status === 'ditolak' && !$request->admin_feedback) { $laporan->admin_feedback = 'Laporan ditolak'; } // Update timestamp sesuai status if ($request->status === 'diproses') { $laporan->diterima_pada = now(); } if ($request->status === 'selesai') { $laporan->selesai_pada = now(); } $laporan->save(); return back()->with('success', 'Tanggapan berhasil dikirim'); }
KategoriController
Mengelola data kategori laporan. Hanya dapat diakses oleh Admin.
- Path:
app/Http/Controllers/KategoriController.php - Middleware:
auth,role:admin - Form Requests:
StoreKategoriRequest,UpdateKategoriRequest - Route:
Route::resource('kategori', KategoriController::class)
public function index() { $kategoris = Kategori::withCount('laporans')->latest()->get(); return view('Admin.Kategori.index', compact('kategoris')); }
public function create() { return view('Admin.Kategori.create'); }
StoreKategoriRequest.public function store(StoreKategoriRequest $request) { Kategori::create($request->validated()); return redirect()->route('kategori.index') ->with('success', 'Kategori berhasil ditambahkan'); }
public function edit(Kategori $kategori) { return view('Admin.Kategori.edit', compact('kategori')); }
public function update(UpdateKategoriRequest $request, Kategori $kategori) { $kategori->update($request->validated()); return redirect()->route('kategori.index') ->with('success', 'Kategori berhasil diperbarui'); }
cascadeOnDelete atau foreign key constraint).public function destroy(Kategori $kategori) { $kategori->delete(); return back()->with('success', 'Kategori berhasil dihapus'); }
TanggapanController
Mengelola komentar/tanggapan pada laporan. Bisa diisi oleh admin maupun siswa.
- Path:
app/Http/Controllers/TanggapanController.php - Middleware:
auth - Form Requests:
StoreTanggapanRequest,UpdateTanggapanRequest - Model:
Tanggapan,Laporan
public function index() { $tanggapans = Tanggapan::with(['user', 'laporan']) ->latest()->get(); return view('Admin.Tanggapan.index', compact('tanggapans')); }
user_id otomatis diambil dari session login.public function store(StoreTanggapanRequest $request) { Tanggapan::create([ ...$request->validated(), 'user_id' => auth()->id(), ]); return back()->with('success', 'Tanggapan berhasil dikirim'); }
public function update(UpdateTanggapanRequest $request, Tanggapan $tanggapan) { $tanggapan->update($request->validated()); return back()->with('success', 'Tanggapan diperbarui'); }
TanggapanPolicy agar hanya pembuat atau admin yang bisa menghapus.public function destroy(Tanggapan $tanggapan) { $tanggapan->delete(); return back()->with('success', 'Tanggapan dihapus'); }
ProfileController
Mengelola profil user yang sedang login — bawaan Laravel Breeze.
public function edit(Request $request): View { return view('profile.edit', [ 'user' => $request->user(), ]); }
email_verified_at di-reset ke null agar user verifikasi ulang.public function update(ProfileUpdateRequest $request): RedirectResponse { $request->user()->fill($request->validated()); if ($request->user()->isDirty('email')) { $request->user()->email_verified_at = null; } $request->user()->save(); return redirect()->route('profile.edit') ->with('status', 'profile-updated'); }
public function destroy(Request $request): RedirectResponse { $request->validateWithBag('userDeletion', [ 'password' => ['required', 'current_password'], ]); $user = $request->user(); Auth::logout(); $user->delete(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect('/'); }
Auth Controllers
Tujuh controller bawaan Laravel 12 Breeze untuk autentikasi pengguna.
| Controller | Method Utama | Fungsi |
|---|---|---|
AuthenticatedSessionController | create, store, destroy | Form login, proses login, logout |
RegisteredUserController | create, store | Form & proses registrasi, assign role default siswa |
PasswordResetLinkController | create, store | Form & kirim link reset password via email |
NewPasswordController | create, store | Form & proses set password baru |
PasswordController | update | Update password dari halaman profil |
EmailVerificationPromptController | __invoke | Tampilkan halaman verifikasi email |
VerifyEmailController | __invoke | Proses verifikasi email dari link |
siswa. Setelah daftar, langsung login dan redirect ke dashboard siswa.public function store(Request $request): RedirectResponse { $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users'], 'password' => ['required', 'confirmed', Password::defaults()], ]); $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), 'role' => 'siswa', // role default ]); Auth::login($user); return redirect(route('siswa.dashboard')); }
admin.dashboard atau siswa.dashboard).public function store(LoginRequest $request): RedirectResponse { $request->authenticate(); $request->session()->regenerate(); $role = auth()->user()->role; return redirect()->intended( $role === 'admin' ? route('admin.dashboard') : route('siswa.dashboard') ); }
Models
Eloquent models beserta relasi dan atribut yang dapat diisi.
class Laporan extends Model { protected $fillable = [ 'user_id', 'kategori_id', 'admin_id', 'lokasi', 'deskripsi', 'foto', 'status', 'admin_feedback', 'diterima_pada', 'ditanggapi_pada', 'selesai_pada', ]; protected $casts = [ 'diterima_pada' => 'datetime', 'ditanggapi_pada' => 'datetime', 'selesai_pada' => 'datetime', ]; public function user() { return $this->belongsTo(User::class); } public function kategori() { return $this->belongsTo(Kategori::class); } public function admin() { return $this->belongsTo(User::class, 'admin_id'); } public function tanggapans() { return $this->hasMany(Tanggapan::class); } }
class Kategori extends Model { protected $fillable = ['nama', 'deskripsi']; public function laporans() { return $this->hasMany(Laporan::class); } }
class User extends Authenticatable { protected $fillable = ['name', 'email', 'password', 'role']; protected $hidden = ['password', 'remember_token']; protected $casts = ['password' => 'hashed']; public function laporans() { return $this->hasMany(Laporan::class); } public function tanggapans() { return $this->hasMany(Tanggapan::class); } public function isAdmin(): bool { return $this->role === 'admin'; } }
class Tanggapan extends Model { protected $fillable = ['laporan_id', 'user_id', 'isi']; public function laporan() { return $this->belongsTo(Laporan::class); } public function user() { return $this->belongsTo(User::class); } }
Seeders & Factories
Mengisi database dengan data awal dan data dummy untuk pengembangan.
php artisan db:seed.public function run(): void { // Buat 1 akun admin User::factory()->create([ 'name' => 'Administrator', 'email' => 'admin@ukk.test', 'role' => 'admin', ]); // Panggil seeder lain $this->call([ KategoriSeeder::class, LaporanSeeder::class, TanggapanSeeder::class, ]); }
kategoris dengan data awal kategori laporan sekolah.public function run(): void { $kategoris = [ ['nama' => 'Sarana & Prasarana', 'deskripsi' => 'Kerusakan fasilitas sekolah'], ['nama' => 'Kebersihan', 'deskripsi' => 'Masalah kebersihan lingkungan'], ['nama' => 'Keamanan', 'deskripsi' => 'Insiden keamanan di sekolah'], ['nama' => 'Lainnya', 'deskripsi' => 'Kategori umum lainnya'], ]; foreach ($kategoris as $k) { Kategori::create($k); } }
Middleware
Middleware custom untuk membatasi akses berdasarkan role pengguna.
class RoleMiddleware { public function handle(Request $request, Closure $next, string $role): Response { if (!auth()->check() || auth()->user()->role !== $role) { return redirect('/')->with('error', 'Akses ditolak.'); } return $next($request); } }
Kernel.php. Middleware alias didaftarkan langsung di bootstrap/app.php.
// bootstrap/app.php return Application::configure(basePath: dirname(__DIR__)) ->withRouting(web: __DIR__.'/../routes/web.php') ->withMiddleware(function (Middleware $middleware) { // Daftarkan alias 'role' $middleware->alias([ 'role' => \App\Http\Middleware\RoleMiddleware::class, ]); }) ->create();
Form Requests
Kelas validasi yang memisahkan logika validasi dari controller.
| Class | Dipakai di | Field yang Divalidasi |
|---|---|---|
StoreLaporanRequest | LaporanController@store | kategori_id*, lokasi*, deskripsi*, foto? |
UpdateLaporanRequest | LaporanController@update | kategori_id*, lokasi*, deskripsi*, foto? |
StoreKategoriRequest | KategoriController@store | nama*, deskripsi? |
UpdateKategoriRequest | KategoriController@update | nama*, deskripsi? |
StoreTanggapanRequest | TanggapanController@store | laporan_id*, isi* |
UpdateTanggapanRequest | TanggapanController@update | isi* |
ProfileUpdateRequest | ProfileController@update | name*, email* |
* = required, ? = nullable/opsional
public function rules(): array { return [ 'kategori_id' => ['required', 'exists:kategoris,id'], 'lokasi' => ['required', 'string', 'max:255'], 'deskripsi' => ['required', 'string'], 'foto' => ['nullable', 'image', 'mimes:jpg,png,jpeg', 'max:2048'], ]; }
Views (Blade)
Halaman tampilan yang dibuat dengan Blade — bawaan Laravel 12 Breeze stack.
| File | Route | Fungsi |
|---|---|---|
Admin/dashboard.blade.php | /admin/dashboard | Statistik ringkasan & laporan terbaru |
Admin/Kategori/index.blade.php | /admin/kategori | Tabel semua kategori + tombol aksi |
Admin/Kategori/create.blade.php | /admin/kategori/create | Form tambah kategori |
Admin/Kategori/edit.blade.php | /admin/kategori/{id}/edit | Form edit kategori |
Admin/Laporan/tanggapi.blade.php | /admin/laporan/{id}/tanggapi | Form feedback + update status laporan |
Admin/Tanggapan/index.blade.php | /admin/tanggapan | Daftar semua tanggapan |
| File | Route | Fungsi |
|---|---|---|
Siswa/dashboard.blade.php | /siswa/dashboard | Ringkasan laporan milik siswa |
Siswa/Laporan/index.blade.php | /laporan | Daftar laporan + status |
Siswa/Laporan/create.blade.php | /laporan/create | Form kirim laporan baru |
Siswa/Laporan/edit.blade.php | /laporan/{id}/edit | Form edit laporan |
| File | Fungsi |
|---|---|
layouts/app.blade.php | Layout utama (user terautentikasi) — include navigation |
layouts/guest.blade.php | Layout halaman tamu (login/register) |
layouts/navigation.blade.php | Navbar responsif — menu beda untuk admin vs siswa |
components/text-input.blade.php | Input field reusable dengan styling Tailwind |
components/primary-button.blade.php | Tombol submit utama |
components/input-error.blade.php | Tampilkan pesan error validasi per field |
components/input-label.blade.php | Label form dengan styling konsisten |
components/modal.blade.php | Komponen modal konfirmasi (delete, dll) |
components/dropdown.blade.php | Dropdown menu responsif |
Policies
Authorization policies untuk mengontrol siapa yang boleh melakukan aksi tertentu.
| Policy | Model | Method yang Dikontrol |
|---|---|---|
LaporanPolicy | Laporan | update, delete — hanya pembuat laporan |
KategoriPolicy | Kategori | viewAny, create, update, delete — hanya admin |
TanggapanPolicy | Tanggapan | update, delete — pembuat atau admin |
class LaporanPolicy { public function update(User $user, Laporan $laporan): bool { // Hanya pembuat laporan yang boleh edit return $user->id === $laporan->user_id; } public function delete(User $user, Laporan $laporan): bool { return $user->id === $laporan->user_id; } }
// Di controller, cukup gunakan $this->authorize() public function destroy(Laporan $laporan) { $this->authorize('delete', $laporan); // LaporanPolicy@delete $laporan->delete(); return back()->with('success', 'Laporan dihapus'); }
AuthServiceProvider.