Tutorial Vibe Coding Laravel 12 dan Filament 4 Projek Website Penerimaan Mahasiswa Baru (PMB) dengan Claude AI

Pelajari cara membangun sistem Penerimaan Mahasiswa Baru (PMB) online menggunakan Laravel 12 dan Filament 4 dengan pendekatan vibe coding 80% prompt 20% review. Tutorial lengkap menggunakan Claude AI untuk developer Indonesia yang ingin membangun aplikasi kampus modern dengan fitur pendaftaran multi-step, pembayaran Midtrans, dan generate kartu ujian PDF.

Bagian 1: Opening — Kenapa Sistem PMB Butuh Modernisasi

Saya pernah bantu satu universitas swasta di Jawa Timur yang masih pakai sistem PMB "semi-manual". Pendaftaran online — tapi cuma form Google Forms. Data masuk ke spreadsheet. Verifikasi berkas? Print satu-satu, cek manual, kasih stempel.

Hasilnya? Setiap gelombang pendaftaran, tim PMB kerja lembur berminggu-minggu. Data duplikat di mana-mana. Ada pendaftar yang bayar tapi tidak tercatat. Ada yang sudah verifikasi tapi kartu ujiannya hilang.

Ini tahun 2026. Sistem seperti itu seharusnya sudah tidak ada.

Masalah Sistem PMB Manual

Mari kita breakdown masalah dari tiga perspektif:

Perspektif Calon Mahasiswa:

  • Harus datang ke kampus berkali-kali untuk submit berkas
  • Tidak tahu status pendaftaran real-time
  • Antri panjang untuk verifikasi dan ambil kartu ujian
  • Kalau ada berkas kurang, harus balik lagi

Perspektif Orang Tua:

  • Tidak bisa monitor progress anak
  • Khawatir pembayaran tidak tercatat
  • Tidak ada transparansi proses seleksi

Perspektif Panitia PMB:

  • Data tersebar di banyak spreadsheet
  • Verifikasi manual = human error tinggi
  • Rekap laporan butuh waktu berhari-hari
  • Koordinasi antar divisi sulit

Solusi: Sistem PMB Online Terintegrasi

Yang akan kita bangun di tutorial ini adalah sistem PMB lengkap:

FLOW SISTEM PMB ONLINE

┌─────────────────────────────────────────────────────────┐
│  CALON MAHASISWA                                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. Registrasi Akun (email + password)                  │
│              ↓                                          │
│  2. Pilih Jalur & Program Studi                         │
│     • Jalur: Reguler / Prestasi / KIP-Kuliah            │
│     • Prodi: Pilihan 1 dan Pilihan 2                    │
│              ↓                                          │
│  3. Isi Formulir Biodata (5 tahap)                      │
│     • Data pribadi (NIK, alamat lengkap)                │
│     • Data orang tua/wali                               │
│     • Data pendidikan (sekolah, nilai rapor)            │
│     • Upload dokumen (foto, KTP, ijazah)                │
│              ↓                                          │
│  4. Bayar Biaya Pendaftaran                             │
│     • Midtrans: VA, QRIS, GoPay, dll                    │
│              ↓                                          │
│  5. Tunggu Verifikasi Admin                             │
│              ↓                                          │
│  6. Cetak Kartu Ujian (PDF)                             │
│              ↓                                          │
│  7. Ikut Ujian                                          │
│              ↓                                          │
│  8. Cek Pengumuman Kelulusan                            │
│              ↓                                          │
│  9. Daftar Ulang + Bayar UKT (jika lulus)               │
│                                                         │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  ADMIN / PANITIA PMB                                    │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  • Kelola master data (fakultas, prodi, gelombang)      │
│  • Verifikasi berkas pendaftar                          │
│  • Atur jadwal dan ruang ujian                          │
│  • Input nilai ujian                                    │
│  • Generate pengumuman kelulusan                        │
│  • Export data ke Excel                                 │
│  • Monitor statistik real-time                          │
│                                                         │
└─────────────────────────────────────────────────────────┘

Manual vs Online: Perbandingan

AspekManual/SpreadsheetSistem Online
PendaftaranDatang ke kampusKapan saja, di mana saja
Submit berkasPrint + antriUpload langsung
Cek statusTelepon panitiaReal-time di dashboard
PembayaranTransfer + konfirmasi manualAuto-verified via Midtrans
VerifikasiCek fisik satu-satuDigital dengan checklist
Kartu ujianAmbil di kampusDownload PDF
PengumumanTempel di papanCek online dengan nomor peserta
LaporanRekap manual berhari-hariExport Excel sekali klik
Human errorTinggiMinimal

Kenapa Vibe Coding Cocok untuk Project Ini

Sistem PMB punya banyak bagian yang repetitif:

  • CRUD untuk master data (fakultas, prodi, gelombang)
  • Form dengan banyak field (biodata lengkap)
  • Status management (draft → submitted → verified → ...)
  • PDF generation dari template
  • Export data ke format tertentu

Ini semua adalah pattern yang AI sangat bagus dalam generate. Dengan pendekatan 80/20:

80% dari prompt ke Claude:

  • Migrations dan models
  • Filament resources (CRUD)
  • Form wizard steps
  • PDF template
  • Export logic

20% review dan edit manual:

  • Validation rules spesifik Indonesia (NIK, format alamat)
  • Business logic seleksi (rumus ranking, passing grade)
  • Security (permission, data access)
  • Edge cases (pendaftar batal, refund)

Tech Stack yang Akan Kita Pakai

TechnologyFungsi
Laravel 12Backend framework
Filament 4Admin panel + Applicant portal
Blade + TailwindFrontend pages
MidtransPayment gateway
DomPDFGenerate kartu ujian PDF
Laravel ExcelExport data pendaftar
Claude AIVibe coding assistant

Kenapa Filament 4 dengan dual panel?

Filament 4 support multiple panels dalam satu aplikasi. Kita akan buat:

  1. Admin Panel (/admin) — untuk panitia PMB
    • Kelola master data
    • Verifikasi pendaftar
    • Input nilai
    • Generate pengumuman
  2. Applicant Panel (/pendaftaran) — untuk calon mahasiswa
    • Isi formulir pendaftaran
    • Upload dokumen
    • Bayar biaya
    • Cetak kartu ujian
    • Cek hasil

Ini lebih elegant daripada bikin 2 aplikasi terpisah atau mixed routes yang berantakan.

Yang Akan Kita Pelajari

Di akhir tutorial ini, kamu akan bisa:

✓ Setup Filament 4 dengan multiple panels
✓ Design database untuk sistem akademik kompleks
✓ Build multi-step form wizard dengan validation
✓ Handle file uploads (foto, dokumen)
✓ Integrate Midtrans untuk pembayaran
✓ Generate PDF (kartu ujian, bukti daftar ulang)
✓ Export data ke Excel
✓ Implement status workflow
✓ Build public announcement page

Mindset Sebelum Mulai

Sistem PMB adalah sistem yang kompleks dengan banyak stakeholder. Tapi dengan pendekatan vibe coding, kita akan pecah jadi bagian-bagian kecil yang manageable.

Setiap bagian akan punya pattern yang sama:

  1. Prompt — jelaskan ke Claude apa yang kamu mau
  2. Review — cek output, pastikan sesuai kebutuhan
  3. Edit — adjust untuk edge cases dan context spesifik
  4. Test — pastikan working sebelum lanjut
💡 TIPS PROJECT SISTEM KAMPUS:

1. Data akademik itu sensitif — perhatikan security
2. Tanggal-tanggal penting (deadline) harus strict
3. Status flow harus jelas dan tidak bisa di-bypass
4. Backup data sebelum setiap operasi batch
5. Log semua perubahan untuk audit trail

Sistem PMB yang baik bukan cuma soal teknologi,
tapi juga soal SOP yang jelas di belakangnya.

Siap membangun sistem PMB modern? Di bagian selanjutnya, kita akan setup project Laravel 12 dengan Filament 4 dual panel.

Bagian 2: Setup Project Laravel 12 + Filament 4 Dual Panel

Di bagian ini kita akan setup foundation project — Laravel 12 dengan Filament 4 yang punya dua panel terpisah: Admin untuk panitia PMB dan Applicant untuk calon mahasiswa.

Prompt 1: Install Laravel 12

Saya mau memulai project sistem PMB (Penerimaan Mahasiswa Baru)
dengan Laravel 12.

Berikan:
1. Command untuk create project baru dengan nama "pmb-online"
2. Konfigurasi .env untuk database MySQL
3. Command untuk generate application key dan jalankan migration

Database name: pmb_online_db

Response Claude (80%):

# 1. Create project Laravel 12
composer create-project laravel/laravel pmb-online "12.*"

cd pmb-online

# 2. Edit file .env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pmb_online_db
DB_USERNAME=root
DB_PASSWORD=

# 3. Buat database
mysql -u root -p -e "CREATE DATABASE pmb_online_db"

# 4. Generate key dan migrate
php artisan key:generate
php artisan migrate

Manual Review (20%):

  • ✅ Laravel 12 dengan wildcard version
  • ✅ Database naming convention
  • ⚠️ Sesuaikan DB_PASSWORD dengan setup lokal

Prompt 2: Install Filament 4 dengan Dual Panel

Install Filament 4 di project Laravel dan setup 2 panels:

1. Admin Panel:
   - Path: /admin
   - Untuk: Panitia PMB (staff universitas)
   - Auth: guard 'web' dengan role check

2. Applicant Panel:
   - Path: /pendaftaran
   - Untuk: Calon mahasiswa
   - Auth: guard 'web' (user biasa)

Kedua panel pakai tabel users yang sama, tapi dibedakan
dengan kolom 'role' (admin/staff/applicant).

Response Claude (80%):

# Install Filament 4
composer require filament/filament:"^4.0"

# Install panel pertama (Admin)
php artisan filament:install --panels

# Saat ditanya:
# - Panel ID: admin
# - Path: admin

Setelah install, kita perlu buat panel kedua secara manual dan modify User model.

Migration untuk tambah role:

<?php

// database/migrations/xxxx_add_role_to_users_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->enum('role', ['admin', 'staff', 'applicant'])
                  ->default('applicant')
                  ->after('password');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('role');
        });
    }
};

Update User Model:

<?php

// app/Models/User.php

namespace App\\Models;

use Filament\\Models\\Contracts\\FilamentUser;
use Filament\\Panel;
use Illuminate\\Foundation\\Auth\\User as Authenticatable;
use Illuminate\\Notifications\\Notifiable;

class User extends Authenticatable implements FilamentUser
{
    use Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'role',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    public function canAccessPanel(Panel $panel): bool
    {
        if ($panel->getId() === 'admin') {
            return in_array($this->role, ['admin', 'staff']);
        }

        if ($panel->getId() === 'applicant') {
            return $this->role === 'applicant';
        }

        return false;
    }

    public function isAdmin(): bool
    {
        return $this->role === 'admin';
    }

    public function isStaff(): bool
    {
        return $this->role === 'staff';
    }

    public function isApplicant(): bool
    {
        return $this->role === 'applicant';
    }
}

Manual Review (20%):

  • ✅ FilamentUser interface implemented
  • ✅ canAccessPanel untuk control akses per panel
  • ✅ Helper methods untuk role checking

Prompt 3: Konfigurasi Admin Panel Provider

Konfigurasi AdminPanelProvider dengan:
- Warna primary hijau (tema kampus)
- Brand name "PMB Universitas Nusantara"
- Logo dari /images/logo.png
- Navigation groups: Master Data, Pendaftaran, Ujian, Laporan
- Dark mode enabled
- Favicon

Response Claude (80%):

<?php

// app/Providers/Filament/AdminPanelProvider.php

namespace App\\Providers\\Filament;

use Filament\\Http\\Middleware\\Authenticate;
use Filament\\Http\\Middleware\\DisableBladeIconComponents;
use Filament\\Http\\Middleware\\DispatchServingFilamentEvent;
use Filament\\Pages;
use Filament\\Panel;
use Filament\\PanelProvider;
use Filament\\Support\\Colors\\Color;
use Filament\\Widgets;
use Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse;
use Illuminate\\Cookie\\Middleware\\EncryptCookies;
use Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken;
use Illuminate\\Routing\\Middleware\\SubstituteBindings;
use Illuminate\\Session\\Middleware\\AuthenticateSession;
use Illuminate\\Session\\Middleware\\StartSession;
use Illuminate\\View\\Middleware\\ShareErrorsFromSession;

class AdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->default()
            ->id('admin')
            ->path('admin')
            ->login()
            ->colors([
                'primary' => Color::Emerald,
                'danger' => Color::Rose,
                'warning' => Color::Amber,
                'success' => Color::Green,
                'info' => Color::Sky,
            ])
            ->brandName('PMB Universitas Nusantara')
            ->brandLogo(asset('images/logo.png'))
            ->brandLogoHeight('2.5rem')
            ->favicon(asset('favicon.ico'))
            ->darkMode(true)
            ->navigationGroups([
                'Master Data',
                'Pendaftaran',
                'Ujian',
                'Laporan',
            ])
            ->sidebarCollapsibleOnDesktop()
            ->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\\\Filament\\\\Admin\\\\Resources')
            ->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\\\Filament\\\\Admin\\\\Pages')
            ->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\\\Filament\\\\Admin\\\\Widgets')
            ->pages([
                Pages\\Dashboard::class,
            ])
            ->widgets([
                Widgets\\AccountWidget::class,
            ])
            ->middleware([
                EncryptCookies::class,
                AddQueuedCookiesToResponse::class,
                StartSession::class,
                AuthenticateSession::class,
                ShareErrorsFromSession::class,
                VerifyCsrfToken::class,
                SubstituteBindings::class,
                DisableBladeIconComponents::class,
                DispatchServingFilamentEvent::class,
            ])
            ->authMiddleware([
                Authenticate::class,
            ]);
    }
}

Manual Review (20%):

  • ✅ Warna Emerald (hijau kampus)
  • ✅ Navigation groups sesuai kebutuhan PMB
  • ✅ Resource path di folder Admin (untuk separation)
  • ⚠️ Buat folder /public/images/ dan taruh logo

Prompt 4: Buat Applicant Panel Provider

Buatkan ApplicantPanelProvider untuk calon mahasiswa dengan:
- Path: /pendaftaran
- Warna primary biru
- Brand name "Portal Pendaftaran - Universitas Nusantara"
- Tanpa navigation groups (simpler UI)
- Registration enabled (calon mahasiswa bisa daftar akun)
- Email verification required
- Profile page untuk edit data diri

Response Claude (80%):

<?php

// app/Providers/Filament/ApplicantPanelProvider.php

namespace App\\Providers\\Filament;

use Filament\\Http\\Middleware\\Authenticate;
use Filament\\Http\\Middleware\\DisableBladeIconComponents;
use Filament\\Http\\Middleware\\DispatchServingFilamentEvent;
use Filament\\Pages;
use Filament\\Panel;
use Filament\\PanelProvider;
use Filament\\Support\\Colors\\Color;
use Filament\\Widgets;
use Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse;
use Illuminate\\Cookie\\Middleware\\EncryptCookies;
use Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken;
use Illuminate\\Routing\\Middleware\\SubstituteBindings;
use Illuminate\\Session\\Middleware\\AuthenticateSession;
use Illuminate\\Session\\Middleware\\StartSession;
use Illuminate\\View\\Middleware\\ShareErrorsFromSession;
use App\\Filament\\Applicant\\Pages\\Registration;

class ApplicantPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->id('applicant')
            ->path('pendaftaran')
            ->login()
            ->registration(Registration::class)
            ->passwordReset()
            ->emailVerification()
            ->profile()
            ->colors([
                'primary' => Color::Blue,
                'danger' => Color::Rose,
                'warning' => Color::Amber,
                'success' => Color::Green,
            ])
            ->brandName('Portal Pendaftaran')
            ->brandLogo(asset('images/logo.png'))
            ->brandLogoHeight('2.5rem')
            ->favicon(asset('favicon.ico'))
            ->darkMode(false)
            ->topNavigation()
            ->discoverResources(in: app_path('Filament/Applicant/Resources'), for: 'App\\\\Filament\\\\Applicant\\\\Resources')
            ->discoverPages(in: app_path('Filament/Applicant/Pages'), for: 'App\\\\Filament\\\\Applicant\\\\Pages')
            ->discoverWidgets(in: app_path('Filament/Applicant/Widgets'), for: 'App\\\\Filament\\\\Applicant\\\\Widgets')
            ->pages([
                Pages\\Dashboard::class,
            ])
            ->widgets([
                Widgets\\AccountWidget::class,
            ])
            ->middleware([
                EncryptCookies::class,
                AddQueuedCookiesToResponse::class,
                StartSession::class,
                AuthenticateSession::class,
                ShareErrorsFromSession::class,
                VerifyCsrfToken::class,
                SubstituteBindings::class,
                DisableBladeIconComponents::class,
                DispatchServingFilamentEvent::class,
            ])
            ->authMiddleware([
                Authenticate::class,
            ]);
    }
}

Custom Registration Page:

<?php

// app/Filament/Applicant/Pages/Registration.php

namespace App\\Filament\\Applicant\\Pages;

use Filament\\Forms\\Components\\TextInput;
use Filament\\Forms\\Form;
use Filament\\Pages\\Auth\\Register as BaseRegister;

class Registration extends BaseRegister
{
    public function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('name')
                    ->label('Nama Lengkap')
                    ->required()
                    ->maxLength(255),

                $this->getEmailFormComponent()
                    ->label('Email'),

                $this->getPasswordFormComponent()
                    ->label('Password'),

                $this->getPasswordConfirmationFormComponent()
                    ->label('Konfirmasi Password'),
            ]);
    }

    protected function mutateFormDataBeforeRegister(array $data): array
    {
        $data['role'] = 'applicant';

        return $data;
    }
}

Manual Review (20%):

  • ✅ Separate panel dengan path berbeda
  • ✅ Registration dengan auto-set role applicant
  • ✅ Email verification enabled
  • ✅ Top navigation untuk simpler UI
  • ⚠️ Setup mail config untuk verification email

Prompt 5: Register Panel Providers

Bagaimana cara register kedua PanelProvider di Laravel 12?

Response Claude (80%):

<?php

// bootstrap/providers.php

return [
    App\\Providers\\AppServiceProvider::class,
    App\\Providers\\Filament\\AdminPanelProvider::class,
    App\\Providers\\Filament\\ApplicantPanelProvider::class,
];

Manual Review (20%):

  • ✅ Kedua provider registered
  • Pastikan urutan AdminPanelProvider sebelum ApplicantPanelProvider (admin as default)

Setup Mail untuk Email Verification

# .env - untuk development pakai Mailtrap atau log

MAIL_MAILER=log
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="PMB Universitas Nusantara"

Untuk production, ganti ke SMTP provider seperti Mailtrap, SendGrid, atau mailserver kampus.

Buat Folder Structure

# Buat folder untuk kedua panels
mkdir -p app/Filament/Admin/Resources
mkdir -p app/Filament/Admin/Pages
mkdir -p app/Filament/Admin/Widgets

mkdir -p app/Filament/Applicant/Resources
mkdir -p app/Filament/Applicant/Pages
mkdir -p app/Filament/Applicant/Widgets

# Buat folder untuk logo
mkdir -p public/images

Buat Admin User untuk Testing

php artisan tinker
use App\\Models\\User;

User::create([
    'name' => 'Admin PMB',
    'email' => '[email protected]',
    'password' => bcrypt('password123'),
    'role' => 'admin',
    'email_verified_at' => now(),
]);

Test Akses Panels

php artisan serve

Buka browser dan test:

PanelURLCredentials
Adminhttp://localhost:8000/admin[email protected] / password123
Applicanthttp://localhost:8000/pendaftaran(register akun baru)

Pastikan:

  • ✅ Admin bisa login ke /admin
  • ✅ Admin TIDAK bisa akses /pendaftaran
  • ✅ Applicant bisa register di /pendaftaran
  • ✅ Applicant TIDAK bisa akses /admin

Struktur Project Sekarang

pmb-online/
├── app/
│   ├── Filament/
│   │   ├── Admin/           ← Resources untuk panitia
│   │   │   ├── Pages/
│   │   │   ├── Resources/
│   │   │   └── Widgets/
│   │   └── Applicant/       ← Resources untuk pendaftar
│   │       ├── Pages/
│   │       │   └── Registration.php
│   │       ├── Resources/
│   │       └── Widgets/
│   ├── Models/
│   │   └── User.php         ← Dengan role & canAccessPanel
│   └── Providers/
│       └── Filament/
│           ├── AdminPanelProvider.php
│           └── ApplicantPanelProvider.php
├── database/
│   └── migrations/
│       └── xxxx_add_role_to_users_table.php
├── public/
│   └── images/              ← Logo kampus
└── bootstrap/
    └── providers.php        ← Both panels registered
💡 RECAP SETUP DUAL PANEL:

Prompts: 5
Files created/modified:
- 1 migration (add role)
- 2 Panel Providers (Admin, Applicant)
- 1 Custom Registration page
- 1 User model update

Key concepts:
- canAccessPanel() untuk control akses
- Separate resource folders per panel
- Custom registration dengan role assignment
- Email verification untuk applicant

Yang di-review manual:
- Role checking logic
- Registration flow
- Mail configuration

Setup selesai! Di bagian selanjutnya, kita akan design database untuk master data — fakultas, program studi, gelombang pendaftaran, dan jalur masuk.

Bagian 3: Database Design — Master Data

Sebelum bisa terima pendaftaran, kita perlu setup master data dulu: fakultas, program studi, gelombang pendaftaran, jalur masuk, dan jadwal ujian. Ini adalah data referensi yang jarang berubah tapi crucial untuk sistem PMB.

Prompt 1: ERD Master Data

Design ERD untuk master data sistem PMB dengan tabel:
1. faculties - Fakultas (contoh: Fakultas Teknik, Fakultas Ekonomi)
2. study_programs - Program Studi, relasi ke faculties
3. admission_waves - Gelombang pendaftaran (Gelombang 1, 2, dst)
4. admission_paths - Jalur masuk (Reguler, Prestasi, KIP-Kuliah)
5. exam_schedules - Jadwal ujian per gelombang
6. announcements - Pengumuman umum

Buatkan dalam format ASCII art dengan penjelasan relasi.

Response Claude (80%):

┌──────────────────┐
│    FACULTIES     │
├──────────────────┤
│ id (PK)          │
│ name             │ "Fakultas Teknik"
│ code             │ "FT"
│ dean_name        │ Nama dekan
│ is_active        │
│ created_at       │
│ updated_at       │
└────────┬─────────┘
         │
         │ 1:N
         ▼
┌──────────────────┐
│  STUDY_PROGRAMS  │
├──────────────────┤
│ id (PK)          │
│ faculty_id (FK)  │───► faculties.id
│ name             │ "Teknik Informatika"
│ code             │ "TI"
│ degree           │ S1/D3/D4
│ quota            │ Kuota per gelombang
│ accreditation    │ A/B/C/Unggul/Baik Sekali
│ tuition_fee      │ Biaya UKT
│ description      │
│ is_active        │
│ created_at       │
│ updated_at       │
└──────────────────┘

┌──────────────────┐
│ ADMISSION_WAVES  │
├──────────────────┤
│ id (PK)          │
│ name             │ "Gelombang 1"
│ year             │ 2026
│ registration_    │ Tanggal buka
│   start_date     │
│ registration_    │ Tanggal tutup
│   end_date       │
│ exam_date        │ Tanggal ujian
│ announcement_    │ Tanggal pengumuman
│   date           │
│ re_registration_ │ Deadline daftar ulang
│   deadline       │
│ status           │ upcoming/open/closed/completed
│ is_active        │
│ created_at       │
│ updated_at       │
└──────────────────┘

┌──────────────────┐
│ ADMISSION_PATHS  │
├──────────────────┤
│ id (PK)          │
│ name             │ "Jalur Reguler"
│ code             │ "REG"
│ registration_fee │ Biaya pendaftaran
│ requirements     │ JSON: syarat-syarat
│ description      │
│ exam_weight      │ JSON: bobot nilai
│ is_active        │
│ created_at       │
│ updated_at       │
└──────────────────┘

┌──────────────────┐       ┌──────────────────┐
│  EXAM_SCHEDULES  │       │  ANNOUNCEMENTS   │
├──────────────────┤       ├──────────────────┤
│ id (PK)          │       │ id (PK)          │
│ admission_wave_  │       │ title            │
│   id (FK)        │───►   │ content          │
│ exam_type        │       │ type             │
│ exam_date        │       │ attachment       │
│ start_time       │       │ is_published     │
│ end_time         │       │ published_at     │
│ rooms            │ JSON  │ created_at       │
│ created_at       │       │ updated_at       │
│ updated_at       │       └──────────────────┘
└──────────────────┘

RELASI:
• Faculty (1) ──── (N) Study Programs
• Admission Wave (1) ──── (N) Exam Schedules

Manual Review (20%):

  • ✅ Relasi one-to-many faculty → programs
  • ✅ Status enum untuk admission_waves
  • ✅ JSON fields untuk flexible data (requirements, rooms)
  • Struktur sudah bagus untuk kebutuhan PMB

Prompt 2: Migration Faculties

Buatkan migration untuk tabel faculties dengan:
- name (nama fakultas)
- code (kode singkat, unique, uppercase)
- dean_name (nama dekan, nullable)
- is_active (default true)
- Index untuk code

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000001_create_faculties_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('faculties', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('code', 10)->unique();
            $table->string('dean_name')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('faculties');
    }
};

Manual Review (20%):

  • ✅ Code dengan max length 10 (cukup untuk kode fakultas)
  • ✅ Unique constraint otomatis create index
  • Tidak perlu edit

Prompt 3: Migration Study Programs

Buatkan migration untuk study_programs dengan:
- Foreign key ke faculties
- name, code (unique per fakultas)
- degree (enum: D3, D4, S1, S2, S3)
- quota (integer, kuota mahasiswa)
- accreditation (enum: A, B, C, Unggul, Baik Sekali, Baik)
- tuition_fee (integer, UKT dalam rupiah)
- description (text, nullable)
- is_active
- Composite index untuk faculty_id + code

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000002_create_study_programs_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('study_programs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('faculty_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('code', 10);
            $table->enum('degree', ['D3', 'D4', 'S1', 'S2', 'S3'])->default('S1');
            $table->integer('quota')->default(0);
            $table->enum('accreditation', [
                'A', 'B', 'C',
                'Unggul', 'Baik Sekali', 'Baik'
            ])->nullable();
            $table->integer('tuition_fee')->default(0);
            $table->text('description')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            $table->unique(['faculty_id', 'code']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('study_programs');
    }
};

Manual Review (20%):

  • ✅ Composite unique untuk faculty_id + code
  • ✅ Accreditation include format baru (Unggul, Baik Sekali, Baik)
  • ✅ Cascade delete — kalau fakultas dihapus, prodi ikut (hati-hati di production!)

Prompt 4: Migration Admission Waves

Buatkan migration untuk admission_waves (gelombang pendaftaran) dengan:
- name ("Gelombang 1", "Gelombang 2", dst)
- year (tahun akademik)
- registration_start_date, registration_end_date
- exam_date
- announcement_date
- re_registration_deadline
- status (enum: upcoming, open, closed, completed)
- is_active
- Index untuk year dan status

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000003_create_admission_waves_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('admission_waves', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->year('year');
            $table->date('registration_start_date');
            $table->date('registration_end_date');
            $table->date('exam_date')->nullable();
            $table->date('announcement_date')->nullable();
            $table->date('re_registration_deadline')->nullable();
            $table->enum('status', [
                'upcoming',   // Belum dibuka
                'open',       // Sedang dibuka
                'closed',     // Pendaftaran ditutup, menunggu ujian
                'completed'   // Sudah selesai semua proses
            ])->default('upcoming');
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            $table->index(['year', 'status']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('admission_waves');
    }
};

Manual Review (20%):

  • ✅ Semua tanggal penting ada
  • ✅ Status enum dengan values yang jelas
  • ✅ Composite index untuk query umum

Prompt 5: Migration Admission Paths

Buatkan migration untuk admission_paths (jalur masuk) dengan:
- name ("Jalur Reguler", "Jalur Prestasi", "Jalur KIP-Kuliah")
- code (REG, PRESTASI, KIP)
- registration_fee (biaya pendaftaran dalam rupiah)
- requirements (JSON array: syarat-syarat jalur)
- description (text)
- exam_weights (JSON object: bobot nilai ujian)
- is_active

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000004_create_admission_paths_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('admission_paths', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('code', 20)->unique();
            $table->integer('registration_fee')->default(0);
            $table->json('requirements')->nullable();
            $table->text('description')->nullable();
            $table->json('exam_weights')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('admission_paths');
    }
};

Contoh data exam_weights:

{
    "written_exam": 40,
    "interview": 30,
    "achievement": 20,
    "report_card": 10
}

Contoh data requirements:

[
    "Lulusan SMA/SMK/MA sederajat",
    "Nilai rata-rata rapor minimal 7.0",
    "Usia maksimal 25 tahun",
    "Sehat jasmani dan rohani"
]

Manual Review (20%):

  • ✅ JSON fields untuk flexibility
  • ✅ Registration fee per jalur (KIP mungkin gratis)

Prompt 6: Migration Exam Schedules

Buatkan migration untuk exam_schedules dengan:
- Foreign key ke admission_waves
- exam_type (enum: written, interview, practical, health_check)
- exam_date
- start_time, end_time
- rooms (JSON array of room info)
- notes (nullable)

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000005_create_exam_schedules_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('exam_schedules', function (Blueprint $table) {
            $table->id();
            $table->foreignId('admission_wave_id')->constrained()->cascadeOnDelete();
            $table->enum('exam_type', [
                'written',      // Ujian tulis
                'interview',    // Wawancara
                'practical',    // Ujian praktik (untuk prodi tertentu)
                'health_check'  // Tes kesehatan
            ]);
            $table->date('exam_date');
            $table->time('start_time');
            $table->time('end_time');
            $table->json('rooms')->nullable();
            $table->text('notes')->nullable();
            $table->timestamps();

            $table->index(['admission_wave_id', 'exam_type']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('exam_schedules');
    }
};

Contoh data rooms:

[
    {"room": "Gedung A - Ruang 101", "capacity": 40},
    {"room": "Gedung A - Ruang 102", "capacity": 40},
    {"room": "Gedung B - Ruang 201", "capacity": 35}
]

Manual Review (20%):

  • ✅ Multiple exam types supported
  • ✅ Flexible rooms dengan JSON

Prompt 7: Migration Announcements

Buatkan migration untuk announcements (pengumuman umum) dengan:
- title
- content (rich text)
- type (enum: info, important, result)
- attachment (file path, nullable)
- is_published
- published_at

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000006_create_announcements_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('announcements', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->longText('content');
            $table->enum('type', [
                'info',      // Informasi umum
                'important', // Pengumuman penting
                'result'     // Pengumuman hasil
            ])->default('info');
            $table->string('attachment')->nullable();
            $table->boolean('is_published')->default(false);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index(['is_published', 'type']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('announcements');
    }
};

Manual Review (20%):

  • ✅ LongText untuk content (support rich text panjang)
  • ✅ Index untuk query published announcements

Prompt 8: Eloquent Models

Buatkan Eloquent models untuk semua tabel master data dengan:
- Proper relationships
- Fillable fields
- Casts untuk JSON dan boolean
- Scopes untuk active records
- Accessors yang berguna

Response Claude (80%):

Faculty Model:

<?php

// app/Models/Faculty.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;

class Faculty extends Model
{
    protected $fillable = [
        'name',
        'code',
        'dean_name',
        'is_active',
    ];

    protected function casts(): array
    {
        return [
            'is_active' => 'boolean',
        ];
    }

    public function studyPrograms(): HasMany
    {
        return $this->hasMany(StudyProgram::class);
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function getFullNameAttribute(): string
    {
        return "{$this->code} - {$this->name}";
    }
}

StudyProgram Model:

<?php

// app/Models/StudyProgram.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;

class StudyProgram extends Model
{
    protected $fillable = [
        'faculty_id',
        'name',
        'code',
        'degree',
        'quota',
        'accreditation',
        'tuition_fee',
        'description',
        'is_active',
    ];

    protected function casts(): array
    {
        return [
            'quota' => 'integer',
            'tuition_fee' => 'integer',
            'is_active' => 'boolean',
        ];
    }

    public function faculty(): BelongsTo
    {
        return $this->belongsTo(Faculty::class);
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function getFullNameAttribute(): string
    {
        return "{$this->degree} {$this->name}";
    }

    public function getFormattedTuitionAttribute(): string
    {
        return 'Rp ' . number_format($this->tuition_fee, 0, ',', '.');
    }

    public function getAccreditationBadgeColorAttribute(): string
    {
        return match($this->accreditation) {
            'A', 'Unggul' => 'success',
            'B', 'Baik Sekali' => 'info',
            'C', 'Baik' => 'warning',
            default => 'gray',
        };
    }
}

AdmissionWave Model:

<?php

// app/Models/AdmissionWave.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
use Carbon\\Carbon;

class AdmissionWave extends Model
{
    protected $fillable = [
        'name',
        'year',
        'registration_start_date',
        'registration_end_date',
        'exam_date',
        'announcement_date',
        're_registration_deadline',
        'status',
        'is_active',
    ];

    protected function casts(): array
    {
        return [
            'year' => 'integer',
            'registration_start_date' => 'date',
            'registration_end_date' => 'date',
            'exam_date' => 'date',
            'announcement_date' => 'date',
            're_registration_deadline' => 'date',
            'is_active' => 'boolean',
        ];
    }

    public function examSchedules(): HasMany
    {
        return $this->hasMany(ExamSchedule::class);
    }

    public function registrations(): HasMany
    {
        return $this->hasMany(Registration::class);
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeOpen($query)
    {
        return $query->where('status', 'open');
    }

    public function scopeCurrentYear($query)
    {
        return $query->where('year', date('Y'));
    }

    public function getFullNameAttribute(): string
    {
        return "{$this->name} - {$this->year}";
    }

    public function getStatusBadgeColorAttribute(): string
    {
        return match($this->status) {
            'upcoming' => 'gray',
            'open' => 'success',
            'closed' => 'warning',
            'completed' => 'info',
            default => 'gray',
        };
    }

    public function getIsOpenAttribute(): bool
    {
        $today = Carbon::today();
        return $this->status === 'open'
            && $today->between($this->registration_start_date, $this->registration_end_date);
    }

    public function getRegistrationPeriodAttribute(): string
    {
        return $this->registration_start_date->format('d M Y')
            . ' - '
            . $this->registration_end_date->format('d M Y');
    }
}

AdmissionPath Model:

<?php

// app/Models/AdmissionPath.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;

class AdmissionPath extends Model
{
    protected $fillable = [
        'name',
        'code',
        'registration_fee',
        'requirements',
        'description',
        'exam_weights',
        'is_active',
    ];

    protected function casts(): array
    {
        return [
            'registration_fee' => 'integer',
            'requirements' => 'array',
            'exam_weights' => 'array',
            'is_active' => 'boolean',
        ];
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function getFormattedFeeAttribute(): string
    {
        if ($this->registration_fee === 0) {
            return 'Gratis';
        }
        return 'Rp ' . number_format($this->registration_fee, 0, ',', '.');
    }
}

ExamSchedule Model:

<?php

// app/Models/ExamSchedule.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;

class ExamSchedule extends Model
{
    protected $fillable = [
        'admission_wave_id',
        'exam_type',
        'exam_date',
        'start_time',
        'end_time',
        'rooms',
        'notes',
    ];

    protected function casts(): array
    {
        return [
            'exam_date' => 'date',
            'start_time' => 'datetime:H:i',
            'end_time' => 'datetime:H:i',
            'rooms' => 'array',
        ];
    }

    public function admissionWave(): BelongsTo
    {
        return $this->belongsTo(AdmissionWave::class);
    }

    public function getExamTypeLabelAttribute(): string
    {
        return match($this->exam_type) {
            'written' => 'Ujian Tulis',
            'interview' => 'Wawancara',
            'practical' => 'Ujian Praktik',
            'health_check' => 'Tes Kesehatan',
            default => $this->exam_type,
        };
    }

    public function getTimeRangeAttribute(): string
    {
        return $this->start_time->format('H:i') . ' - ' . $this->end_time->format('H:i');
    }

    public function getTotalCapacityAttribute(): int
    {
        if (empty($this->rooms)) return 0;

        return collect($this->rooms)->sum('capacity');
    }
}

Announcement Model:

<?php

// app/Models/Announcement.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Support\\Facades\\Storage;

class Announcement extends Model
{
    protected $fillable = [
        'title',
        'content',
        'type',
        'attachment',
        'is_published',
        'published_at',
    ];

    protected function casts(): array
    {
        return [
            'is_published' => 'boolean',
            'published_at' => 'datetime',
        ];
    }

    public function scopePublished($query)
    {
        return $query->where('is_published', true)
                     ->whereNotNull('published_at')
                     ->where('published_at', '<=', now());
    }

    public function getTypeBadgeColorAttribute(): string
    {
        return match($this->type) {
            'info' => 'info',
            'important' => 'danger',
            'result' => 'success',
            default => 'gray',
        };
    }

    public function getTypeLabelAttribute(): string
    {
        return match($this->type) {
            'info' => 'Informasi',
            'important' => 'Penting',
            'result' => 'Hasil Seleksi',
            default => $this->type,
        };
    }

    public function getAttachmentUrlAttribute(): ?string
    {
        return $this->attachment ? Storage::url($this->attachment) : null;
    }
}

Manual Review (20%):

  • ✅ Semua relationships benar
  • ✅ JSON casts untuk array fields
  • ✅ Accessors untuk formatted values
  • ✅ Scopes untuk common queries
  • ✅ Badge color attributes untuk UI

Prompt 9: Seeder Data Awal

Buatkan DatabaseSeeder untuk populate:
- 5 fakultas dengan kode
- 15-20 program studi (3-4 per fakultas)
- 2 gelombang pendaftaran untuk tahun 2026
- 3 jalur masuk (Reguler, Prestasi, KIP-Kuliah)

Pakai data realistis Indonesia.

Response Claude (80%):

<?php

// database/seeders/DatabaseSeeder.php

namespace Database\\Seeders;

use App\\Models\\Faculty;
use App\\Models\\StudyProgram;
use App\\Models\\AdmissionWave;
use App\\Models\\AdmissionPath;
use Illuminate\\Database\\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->seedFacultiesAndPrograms();
        $this->seedAdmissionWaves();
        $this->seedAdmissionPaths();
    }

    private function seedFacultiesAndPrograms(): void
    {
        $faculties = [
            [
                'name' => 'Fakultas Teknik',
                'code' => 'FT',
                'dean_name' => 'Dr. Ir. Bambang Sudrajat, M.T.',
                'programs' => [
                    ['name' => 'Teknik Informatika', 'code' => 'TI', 'degree' => 'S1', 'quota' => 120, 'accreditation' => 'Unggul', 'tuition_fee' => 8500000],
                    ['name' => 'Teknik Elektro', 'code' => 'TE', 'degree' => 'S1', 'quota' => 80, 'accreditation' => 'A', 'tuition_fee' => 8000000],
                    ['name' => 'Teknik Mesin', 'code' => 'TM', 'degree' => 'S1', 'quota' => 80, 'accreditation' => 'A', 'tuition_fee' => 8000000],
                    ['name' => 'Teknik Sipil', 'code' => 'TS', 'degree' => 'S1', 'quota' => 100, 'accreditation' => 'Unggul', 'tuition_fee' => 7500000],
                ],
            ],
            [
                'name' => 'Fakultas Ekonomi dan Bisnis',
                'code' => 'FEB',
                'dean_name' => 'Prof. Dr. Siti Rahayu, S.E., M.M.',
                'programs' => [
                    ['name' => 'Manajemen', 'code' => 'MN', 'degree' => 'S1', 'quota' => 150, 'accreditation' => 'Unggul', 'tuition_fee' => 7000000],
                    ['name' => 'Akuntansi', 'code' => 'AK', 'degree' => 'S1', 'quota' => 120, 'accreditation' => 'A', 'tuition_fee' => 7000000],
                    ['name' => 'Ekonomi Pembangunan', 'code' => 'EP', 'degree' => 'S1', 'quota' => 60, 'accreditation' => 'Baik Sekali', 'tuition_fee' => 6500000],
                ],
            ],
            [
                'name' => 'Fakultas Ilmu Komputer',
                'code' => 'FILKOM',
                'dean_name' => 'Dr. Ahmad Fauzi, S.Kom., M.Cs.',
                'programs' => [
                    ['name' => 'Sistem Informasi', 'code' => 'SI', 'degree' => 'S1', 'quota' => 100, 'accreditation' => 'Unggul', 'tuition_fee' => 8500000],
                    ['name' => 'Teknologi Informasi', 'code' => 'TIF', 'degree' => 'S1', 'quota' => 80, 'accreditation' => 'A', 'tuition_fee' => 8000000],
                    ['name' => 'Data Science', 'code' => 'DS', 'degree' => 'S1', 'quota' => 60, 'accreditation' => 'Baik Sekali', 'tuition_fee' => 9000000],
                ],
            ],
            [
                'name' => 'Fakultas Hukum',
                'code' => 'FH',
                'dean_name' => 'Prof. Dr. Hendra Wijaya, S.H., M.H.',
                'programs' => [
                    ['name' => 'Ilmu Hukum', 'code' => 'IH', 'degree' => 'S1', 'quota' => 120, 'accreditation' => 'A', 'tuition_fee' => 6500000],
                ],
            ],
            [
                'name' => 'Fakultas Ilmu Komunikasi',
                'code' => 'FIKOM',
                'dean_name' => 'Dr. Maya Sari, S.I.Kom., M.I.Kom.',
                'programs' => [
                    ['name' => 'Ilmu Komunikasi', 'code' => 'IKOM', 'degree' => 'S1', 'quota' => 100, 'accreditation' => 'A', 'tuition_fee' => 7000000],
                    ['name' => 'Hubungan Masyarakat', 'code' => 'HUMAS', 'degree' => 'S1', 'quota' => 60, 'accreditation' => 'Baik Sekali', 'tuition_fee' => 6500000],
                    ['name' => 'Jurnalistik', 'code' => 'JRN', 'degree' => 'S1', 'quota' => 50, 'accreditation' => 'B', 'tuition_fee' => 6000000],
                ],
            ],
        ];

        foreach ($faculties as $facultyData) {
            $programs = $facultyData['programs'];
            unset($facultyData['programs']);

            $faculty = Faculty::create($facultyData);

            foreach ($programs as $program) {
                $faculty->studyPrograms()->create($program);
            }
        }
    }

    private function seedAdmissionWaves(): void
    {
        AdmissionWave::create([
            'name' => 'Gelombang 1',
            'year' => 2026,
            'registration_start_date' => '2026-01-15',
            'registration_end_date' => '2026-03-31',
            'exam_date' => '2026-04-15',
            'announcement_date' => '2026-04-30',
            're_registration_deadline' => '2026-05-15',
            'status' => 'open',
            'is_active' => true,
        ]);

        AdmissionWave::create([
            'name' => 'Gelombang 2',
            'year' => 2026,
            'registration_start_date' => '2026-05-01',
            'registration_end_date' => '2026-06-30',
            'exam_date' => '2026-07-10',
            'announcement_date' => '2026-07-25',
            're_registration_deadline' => '2026-08-10',
            'status' => 'upcoming',
            'is_active' => true,
        ]);
    }

    private function seedAdmissionPaths(): void
    {
        AdmissionPath::create([
            'name' => 'Jalur Reguler',
            'code' => 'REG',
            'registration_fee' => 350000,
            'requirements' => [
                'Lulusan SMA/SMK/MA sederajat',
                'Nilai rata-rata rapor minimal 7.0',
                'Usia maksimal 25 tahun saat mendaftar',
                'Sehat jasmani dan rohani',
            ],
            'exam_weights' => [
                'written_exam' => 50,
                'interview' => 30,
                'report_card' => 20,
            ],
            'description' => 'Jalur seleksi umum melalui ujian tulis dan wawancara.',
            'is_active' => true,
        ]);

        AdmissionPath::create([
            'name' => 'Jalur Prestasi',
            'code' => 'PRESTASI',
            'registration_fee' => 250000,
            'requirements' => [
                'Lulusan SMA/SMK/MA sederajat',
                'Nilai rata-rata rapor minimal 8.0',
                'Memiliki prestasi akademik atau non-akademik tingkat kabupaten/kota atau lebih tinggi',
                'Melampirkan sertifikat/piagam prestasi',
            ],
            'exam_weights' => [
                'achievement' => 40,
                'interview' => 35,
                'report_card' => 25,
            ],
            'description' => 'Jalur seleksi bagi siswa berprestasi tanpa ujian tulis.',
            'is_active' => true,
        ]);

        AdmissionPath::create([
            'name' => 'Jalur KIP-Kuliah',
            'code' => 'KIP',
            'registration_fee' => 0,
            'requirements' => [
                'Lulusan SMA/SMK/MA sederajat',
                'Terdaftar sebagai penerima KIP atau dari keluarga kurang mampu',
                'Memiliki SKTM atau dokumen pendukung lainnya',
                'Nilai rata-rata rapor minimal 7.5',
            ],
            'exam_weights' => [
                'written_exam' => 40,
                'interview' => 30,
                'report_card' => 30,
            ],
            'description' => 'Jalur khusus bagi calon mahasiswa dari keluarga kurang mampu. Biaya pendaftaran GRATIS.',
            'is_active' => true,
        ]);
    }
}

Manual Review (20%):

  • ✅ Data realistis Indonesia
  • ✅ UKT dalam range wajar (6-9 juta)
  • ✅ Akreditasi sesuai format baru
  • ✅ KIP dengan registration_fee = 0

Jalankan seeder:

php artisan migrate:fresh --seed
💡 RECAP DATABASE MASTER DATA:

Prompts: 9
Tables created: 6
- faculties
- study_programs
- admission_waves
- admission_paths
- exam_schedules
- announcements

Models created: 6 dengan relationships dan accessors
Seeder: Data awal 5 fakultas, 14 prodi, 2 gelombang, 3 jalur

Yang di-review manual:
- Cascade delete implications
- JSON structure untuk requirements/weights
- Status enum values
- Accreditation format baru

Di bagian selanjutnya, kita akan buat migration untuk tabel utama: registrations — yang menyimpan semua data pendaftar dari biodata sampai hasil seleksi.

Bagian 4: Database Design — Tabel Registrations

Ini adalah tabel paling kompleks di sistem PMB. Tabel registrations menyimpan semua data pendaftar — dari biodata lengkap, data orang tua, riwayat pendidikan, dokumen upload, sampai hasil ujian dan status kelulusan.

Prompt 1: Analisis Fields yang Dibutuhkan

Analisis field-field yang dibutuhkan untuk tabel registrations
di sistem PMB Indonesia. Kelompokkan menjadi:
1. Relasi ke tabel lain
2. Data pribadi pendaftar
3. Data orang tua/wali
4. Data pendidikan
5. Dokumen upload
6. Data ujian dan seleksi
7. Status dan payment
8. Daftar ulang

Perhatikan format data Indonesia (NIK 16 digit, alamat
dengan RT/RW/kelurahan/kecamatan, dll).

Response Claude (80%):

ANALISIS FIELD REGISTRATIONS

1. RELASI KE TABEL LAIN
─────────────────────────────────────────
• user_id           → users (pendaftar)
• admission_wave_id → admission_waves (gelombang)
• admission_path_id → admission_paths (jalur)
• first_choice_id   → study_programs (pilihan 1)
• second_choice_id  → study_programs (pilihan 2)
• accepted_program_id → study_programs (diterima di)

2. DATA PRIBADI
─────────────────────────────────────────
• registration_number (auto-generate, unique)
• nik (16 digit, unique)
• full_name
• nickname
• birth_place
• birth_date
• gender (male/female)
• religion (islam/kristen/katolik/hindu/buddha/konghucu)
• blood_type (A/B/AB/O)
• phone
• email (dari user, readonly)

3. ALAMAT LENGKAP
─────────────────────────────────────────
• address (jalan, nomor rumah)
• rt, rw
• village (kelurahan/desa)
• district (kecamatan)
• city (kabupaten/kota)
• province
• postal_code

4. DATA ORANG TUA
─────────────────────────────────────────
Ayah:
• father_name
• father_nik
• father_birth_date
• father_education (SD/SMP/SMA/D3/S1/S2/S3)
• father_occupation
• father_income (range)
• father_phone

Ibu:
• mother_name
• mother_nik
• mother_birth_date
• mother_education
• mother_occupation
• mother_income
• mother_phone

Wali (jika bukan orang tua kandung):
• guardian_name
• guardian_phone
• guardian_relation

5. DATA PENDIDIKAN
─────────────────────────────────────────
• school_name
• school_npsn (Nomor Pokok Sekolah Nasional)
• school_type (SMA/SMK/MA/Paket C)
• school_address
• school_city
• school_province
• graduation_year
• major (jurusan untuk SMK)

Nilai Rapor (rata-rata per semester):
• semester_1_avg ... semester_5_avg
• average_score (kalkulasi total)

6. DOKUMEN UPLOAD
─────────────────────────────────────────
• photo (pas foto 3x4)
• ktp_scan
• family_card_scan (KK)
• birth_certificate_scan (akta lahir)
• diploma_scan (ijazah/SKL)
• transcript_scan (transkrip nilai)
• health_certificate_scan (surat kesehatan)
• achievement_certificates (JSON array, sertifikat prestasi)
• kip_card_scan (untuk jalur KIP)

7. DATA UJIAN & SELEKSI
─────────────────────────────────────────
• exam_number (nomor peserta ujian)
• exam_room
• exam_seat
• written_exam_score
• interview_score
• achievement_score
• final_score (kalkulasi dengan bobot)
• ranking (per program studi)
• result (passed/failed/waitlist)
• announced_at

8. STATUS & PAYMENT
─────────────────────────────────────────
Status pendaftaran:
• status (enum dengan banyak nilai)
• submitted_at
• verification_notes
• verified_at
• verified_by (admin user_id)

Payment:
• payment_status (unpaid/pending/paid)
• payment_amount
• midtrans_transaction_id
• payment_type
• paid_at

9. DAFTAR ULANG
─────────────────────────────────────────
• re_registration_status (pending/completed)
• nim (Nomor Induk Mahasiswa, assigned saat daftar ulang)
• ukt_amount
• ukt_paid_at
• re_registration_completed_at

Manual Review (20%):

  • ✅ Komprehensif untuk kebutuhan PMB Indonesia
  • ✅ Format Indonesia (NIK, NPSN, RT/RW)
  • ⚠️ Tabel akan sangat lebar — perlu pertimbangkan normalisasi untuk production scale

Prompt 2: Migration Registrations

Buatkan migration untuk tabel registrations berdasarkan
analisis di atas. Pisahkan dengan comment section untuk
readability. Include proper indexes untuk:
- user_id
- admission_wave_id + status
- exam_number
- registration_number
- nik

Response Claude (80%):

<?php

// database/migrations/2024_01_01_000010_create_registrations_table.php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('registrations', function (Blueprint $table) {
            $table->id();

            // ============================================
            // RELASI
            // ============================================
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->foreignId('admission_wave_id')->constrained()->restrictOnDelete();
            $table->foreignId('admission_path_id')->constrained()->restrictOnDelete();
            $table->foreignId('first_choice_id')->constrained('study_programs')->restrictOnDelete();
            $table->foreignId('second_choice_id')->nullable()->constrained('study_programs')->restrictOnDelete();
            $table->foreignId('accepted_program_id')->nullable()->constrained('study_programs')->nullOnDelete();

            // ============================================
            // NOMOR REGISTRASI
            // ============================================
            $table->string('registration_number', 30)->unique();

            // ============================================
            // DATA PRIBADI
            // ============================================
            $table->string('nik', 16)->unique();
            $table->string('full_name');
            $table->string('nickname', 50)->nullable();
            $table->string('birth_place');
            $table->date('birth_date');
            $table->enum('gender', ['male', 'female']);
            $table->enum('religion', [
                'islam', 'kristen', 'katolik', 'hindu', 'buddha', 'konghucu'
            ]);
            $table->enum('blood_type', ['A', 'B', 'AB', 'O'])->nullable();
            $table->string('phone', 20);

            // ============================================
            // ALAMAT
            // ============================================
            $table->text('address');
            $table->string('rt', 5)->nullable();
            $table->string('rw', 5)->nullable();
            $table->string('village', 100); // Kelurahan/Desa
            $table->string('district', 100); // Kecamatan
            $table->string('city', 100); // Kabupaten/Kota
            $table->string('province', 100);
            $table->string('postal_code', 10)->nullable();

            // ============================================
            // DATA AYAH
            // ============================================
            $table->string('father_name');
            $table->string('father_nik', 16)->nullable();
            $table->date('father_birth_date')->nullable();
            $table->enum('father_education', [
                'sd', 'smp', 'sma', 'd1', 'd2', 'd3', 'd4', 's1', 's2', 's3'
            ])->nullable();
            $table->string('father_occupation')->nullable();
            $table->enum('father_income', [
                'below_1m',      // < 1 juta
                '1m_to_3m',      // 1-3 juta
                '3m_to_5m',      // 3-5 juta
                '5m_to_10m',     // 5-10 juta
                'above_10m'      // > 10 juta
            ])->nullable();
            $table->string('father_phone', 20)->nullable();

            // ============================================
            // DATA IBU
            // ============================================
            $table->string('mother_name');
            $table->string('mother_nik', 16)->nullable();
            $table->date('mother_birth_date')->nullable();
            $table->enum('mother_education', [
                'sd', 'smp', 'sma', 'd1', 'd2', 'd3', 'd4', 's1', 's2', 's3'
            ])->nullable();
            $table->string('mother_occupation')->nullable();
            $table->enum('mother_income', [
                'below_1m', '1m_to_3m', '3m_to_5m', '5m_to_10m', 'above_10m'
            ])->nullable();
            $table->string('mother_phone', 20)->nullable();

            // ============================================
            // DATA WALI (OPSIONAL)
            // ============================================
            $table->string('guardian_name')->nullable();
            $table->string('guardian_phone', 20)->nullable();
            $table->string('guardian_relation', 50)->nullable();

            // ============================================
            // DATA PENDIDIKAN
            // ============================================
            $table->string('school_name');
            $table->string('school_npsn', 20)->nullable();
            $table->enum('school_type', ['sma', 'smk', 'ma', 'paket_c']);
            $table->text('school_address')->nullable();
            $table->string('school_city', 100)->nullable();
            $table->string('school_province', 100)->nullable();
            $table->year('graduation_year');
            $table->string('school_major')->nullable(); // Jurusan untuk SMK

            // Nilai Rapor per Semester
            $table->decimal('semester_1_avg', 5, 2)->nullable();
            $table->decimal('semester_2_avg', 5, 2)->nullable();
            $table->decimal('semester_3_avg', 5, 2)->nullable();
            $table->decimal('semester_4_avg', 5, 2)->nullable();
            $table->decimal('semester_5_avg', 5, 2)->nullable();
            $table->decimal('average_score', 5, 2)->nullable(); // Rata-rata keseluruhan

            // ============================================
            // DOKUMEN UPLOAD
            // ============================================
            $table->string('photo')->nullable();
            $table->string('ktp_scan')->nullable();
            $table->string('family_card_scan')->nullable();
            $table->string('birth_certificate_scan')->nullable();
            $table->string('diploma_scan')->nullable();
            $table->string('transcript_scan')->nullable();
            $table->string('health_certificate_scan')->nullable();
            $table->json('achievement_certificates')->nullable(); // Array of file paths
            $table->string('kip_card_scan')->nullable(); // Untuk jalur KIP

            // ============================================
            // DATA UJIAN & SELEKSI
            // ============================================
            $table->string('exam_number', 30)->nullable()->unique();
            $table->string('exam_room', 50)->nullable();
            $table->string('exam_seat', 10)->nullable();
            $table->decimal('written_exam_score', 5, 2)->nullable();
            $table->decimal('interview_score', 5, 2)->nullable();
            $table->decimal('achievement_score', 5, 2)->nullable();
            $table->decimal('final_score', 5, 2)->nullable();
            $table->integer('ranking')->nullable();
            $table->enum('result', ['passed', 'failed', 'waitlist'])->nullable();
            $table->timestamp('announced_at')->nullable();

            // ============================================
            // STATUS PENDAFTARAN
            // ============================================
            $table->enum('status', [
                'draft',           // Masih mengisi form
                'submitted',       // Sudah submit, menunggu bayar
                'paid',            // Sudah bayar, menunggu verifikasi
                'verification_failed', // Dokumen ditolak
                'verified',        // Dokumen diverifikasi
                'exam_scheduled',  // Jadwal ujian sudah ditentukan
                'exam_completed',  // Sudah selesai ujian
                'selection',       // Dalam proses seleksi
                'announced',       // Sudah diumumkan
                'accepted',        // Diterima
                'rejected',        // Tidak diterima
                're_registered',   // Sudah daftar ulang
                'cancelled'        // Dibatalkan
            ])->default('draft');
            $table->timestamp('submitted_at')->nullable();
            $table->text('verification_notes')->nullable();
            $table->timestamp('verified_at')->nullable();
            $table->foreignId('verified_by')->nullable()->constrained('users')->nullOnDelete();

            // ============================================
            // PAYMENT
            // ============================================
            $table->enum('payment_status', ['unpaid', 'pending', 'paid'])->default('unpaid');
            $table->integer('payment_amount')->nullable();
            $table->string('midtrans_transaction_id')->nullable();
            $table->string('payment_type', 50)->nullable();
            $table->timestamp('paid_at')->nullable();

            // ============================================
            // DAFTAR ULANG
            // ============================================
            $table->enum('re_registration_status', ['pending', 'completed'])->nullable();
            $table->string('nim', 20)->nullable()->unique(); // Nomor Induk Mahasiswa
            $table->integer('ukt_amount')->nullable();
            $table->timestamp('ukt_paid_at')->nullable();
            $table->timestamp('re_registration_completed_at')->nullable();

            $table->timestamps();

            // ============================================
            // INDEXES
            // ============================================
            $table->index(['admission_wave_id', 'status']);
            $table->index(['admission_wave_id', 'admission_path_id']);
            $table->index(['first_choice_id', 'status']);
            $table->index(['status', 'payment_status']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('registrations');
    }
};

Manual Review (20%):

  • ✅ Semua field terorganisir dengan comment sections
  • ✅ Proper foreign key constraints (restrict untuk data penting)
  • ✅ Composite indexes untuk query umum
  • ✅ Nullable untuk optional fields
  • ⚠️ Migration ini sangat panjang — di production bisa dipecah jadi beberapa migration

Prompt 3: Registration Model

Buatkan Registration model dengan:
- Semua relationships (belongsTo, hasOne)
- Casts untuk dates, JSON, decimal
- Scopes untuk filter status, wave, path
- Accessors untuk:
  - formatted dates
  - status labels dalam Bahasa Indonesia
  - badge colors
  - full address
  - calculated scores
- Auto-generate registration_number saat creating
- Auto-calculate average_score dari nilai semester

Response Claude (80%):

<?php

// app/Models/Registration.php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
use Illuminate\\Support\\Facades\\Storage;
use Illuminate\\Support\\Str;

class Registration extends Model
{
    protected $fillable = [
        // Relasi
        'user_id',
        'admission_wave_id',
        'admission_path_id',
        'first_choice_id',
        'second_choice_id',
        'accepted_program_id',

        // Identitas
        'registration_number',
        'nik',
        'full_name',
        'nickname',
        'birth_place',
        'birth_date',
        'gender',
        'religion',
        'blood_type',
        'phone',

        // Alamat
        'address',
        'rt',
        'rw',
        'village',
        'district',
        'city',
        'province',
        'postal_code',

        // Data Ayah
        'father_name',
        'father_nik',
        'father_birth_date',
        'father_education',
        'father_occupation',
        'father_income',
        'father_phone',

        // Data Ibu
        'mother_name',
        'mother_nik',
        'mother_birth_date',
        'mother_education',
        'mother_occupation',
        'mother_income',
        'mother_phone',

        // Data Wali
        'guardian_name',
        'guardian_phone',
        'guardian_relation',

        // Data Pendidikan
        'school_name',
        'school_npsn',
        'school_type',
        'school_address',
        'school_city',
        'school_province',
        'graduation_year',
        'school_major',
        'semester_1_avg',
        'semester_2_avg',
        'semester_3_avg',
        'semester_4_avg',
        'semester_5_avg',
        'average_score',

        // Dokumen
        'photo',
        'ktp_scan',
        'family_card_scan',
        'birth_certificate_scan',
        'diploma_scan',
        'transcript_scan',
        'health_certificate_scan',
        'achievement_certificates',
        'kip_card_scan',

        // Ujian
        'exam_number',
        'exam_room',
        'exam_seat',
        'written_exam_score',
        'interview_score',
        'achievement_score',
        'final_score',
        'ranking',
        'result',
        'announced_at',

        // Status
        'status',
        'submitted_at',
        'verification_notes',
        'verified_at',
        'verified_by',

        // Payment
        'payment_status',
        'payment_amount',
        'midtrans_transaction_id',
        'payment_type',
        'paid_at',

        // Daftar Ulang
        're_registration_status',
        'nim',
        'ukt_amount',
        'ukt_paid_at',
        're_registration_completed_at',
    ];

    protected function casts(): array
    {
        return [
            'birth_date' => 'date',
            'father_birth_date' => 'date',
            'mother_birth_date' => 'date',
            'graduation_year' => 'integer',

            'semester_1_avg' => 'decimal:2',
            'semester_2_avg' => 'decimal:2',
            'semester_3_avg' => 'decimal:2',
            'semester_4_avg' => 'decimal:2',
            'semester_5_avg' => 'decimal:2',
            'average_score' => 'decimal:2',

            'written_exam_score' => 'decimal:2',
            'interview_score' => 'decimal:2',
            'achievement_score' => 'decimal:2',
            'final_score' => 'decimal:2',

            'payment_amount' => 'integer',
            'ukt_amount' => 'integer',

            'achievement_certificates' => 'array',

            'submitted_at' => 'datetime',
            'verified_at' => 'datetime',
            'announced_at' => 'datetime',
            'paid_at' => 'datetime',
            'ukt_paid_at' => 'datetime',
            're_registration_completed_at' => 'datetime',
        ];
    }

    // ============================================
    // BOOT
    // ============================================

    protected static function booted(): void
    {
        static::creating(function (Registration $registration) {
            if (empty($registration->registration_number)) {
                $registration->registration_number = self::generateRegistrationNumber($registration);
            }
        });

        static::saving(function (Registration $registration) {
            // Auto-calculate average score
            $registration->calculateAverageScore();
        });
    }

    public static function generateRegistrationNumber(Registration $registration): string
    {
        $wave = AdmissionWave::find($registration->admission_wave_id);
        $path = AdmissionPath::find($registration->admission_path_id);

        $year = $wave?->year ?? date('Y');
        $waveNum = $wave ? Str::padLeft($wave->id, 2, '0') : '01';
        $pathCode = $path?->code ?? 'REG';

        $lastNumber = self::where('admission_wave_id', $registration->admission_wave_id)
            ->where('admission_path_id', $registration->admission_path_id)
            ->max('id') ?? 0;

        $sequence = Str::padLeft($lastNumber + 1, 5, '0');

        return "PMB-{$year}-{$waveNum}-{$pathCode}-{$sequence}";
    }

    public function calculateAverageScore(): void
    {
        $scores = array_filter([
            $this->semester_1_avg,
            $this->semester_2_avg,
            $this->semester_3_avg,
            $this->semester_4_avg,
            $this->semester_5_avg,
        ], fn($score) => $score !== null);

        if (count($scores) > 0) {
            $this->average_score = round(array_sum($scores) / count($scores), 2);
        }
    }

    // ============================================
    // RELATIONSHIPS
    // ============================================

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function admissionWave(): BelongsTo
    {
        return $this->belongsTo(AdmissionWave::class);
    }

    public function admissionPath(): BelongsTo
    {
        return $this->belongsTo(AdmissionPath::class);
    }

    public function firstChoice(): BelongsTo
    {
        return $this->belongsTo(StudyProgram::class, 'first_choice_id');
    }

    public function secondChoice(): BelongsTo
    {
        return $this->belongsTo(StudyProgram::class, 'second_choice_id');
    }

    public function acceptedProgram(): BelongsTo
    {
        return $this->belongsTo(StudyProgram::class, 'accepted_program_id');
    }

    public function verifiedByUser(): BelongsTo
    {
        return $this->belongsTo(User::class, 'verified_by');
    }

    // ============================================
    // SCOPES
    // ============================================

    public function scopeForWave($query, $waveId)
    {
        return $query->where('admission_wave_id', $waveId);
    }

    public function scopeForPath($query, $pathId)
    {
        return $query->where('admission_path_id', $pathId);
    }

    public function scopeWithStatus($query, $status)
    {
        return $query->where('status', $status);
    }

    public function scopeSubmitted($query)
    {
        return $query->whereNotIn('status', ['draft', 'cancelled']);
    }

    public function scopePendingVerification($query)
    {
        return $query->where('status', 'paid');
    }

    public function scopeVerified($query)
    {
        return $query->whereIn('status', ['verified', 'exam_scheduled', 'exam_completed', 'selection', 'announced', 'accepted', 're_registered']);
    }

    public function scopeAccepted($query)
    {
        return $query->whereIn('status', ['accepted', 're_registered']);
    }

    public function scopePendingPayment($query)
    {
        return $query->where('status', 'submitted')
                     ->where('payment_status', 'unpaid');
    }

    // ============================================
    // ACCESSORS - STATUS
    // ============================================

    public function getStatusLabelAttribute(): string
    {
        return match($this->status) {
            'draft' => 'Draft',
            'submitted' => 'Menunggu Pembayaran',
            'paid' => 'Menunggu Verifikasi',
            'verification_failed' => 'Verifikasi Gagal',
            'verified' => 'Terverifikasi',
            'exam_scheduled' => 'Ujian Dijadwalkan',
            'exam_completed' => 'Ujian Selesai',
            'selection' => 'Proses Seleksi',
            'announced' => 'Diumumkan',
            'accepted' => 'Diterima',
            'rejected' => 'Tidak Diterima',
            're_registered' => 'Sudah Daftar Ulang',
            'cancelled' => 'Dibatalkan',
            default => $this->status,
        };
    }

    public function getStatusColorAttribute(): string
    {
        return match($this->status) {
            'draft' => 'gray',
            'submitted' => 'warning',
            'paid' => 'info',
            'verification_failed' => 'danger',
            'verified' => 'success',
            'exam_scheduled' => 'primary',
            'exam_completed' => 'primary',
            'selection' => 'warning',
            'announced' => 'info',
            'accepted' => 'success',
            'rejected' => 'danger',
            're_registered' => 'success',
            'cancelled' => 'gray',
            default => 'gray',
        };
    }

    public function getPaymentStatusLabelAttribute(): string
    {
        return match($this->payment_status) {
            'unpaid' => 'Belum Bayar',
            'pending' => 'Menunggu Konfirmasi',
            'paid' => 'Sudah Bayar',
            default => $this->payment_status,
        };
    }

    public function getResultLabelAttribute(): ?string
    {
        return match($this->result) {
            'passed' => 'LULUS',
            'failed' => 'TIDAK LULUS',
            'waitlist' => 'CADANGAN',
            default => null,
        };
    }

    public function getResultColorAttribute(): string
    {
        return match($this->result) {
            'passed' => 'success',
            'failed' => 'danger',
            'waitlist' => 'warning',
            default => 'gray',
        };
    }

    // ============================================
    // ACCESSORS - DATA FORMATTING
    // ============================================

    public function getGenderLabelAttribute(): string
    {
        return match($this->gender) {
            'male' => 'Laki-laki',
            'female' => 'Perempuan',
            default => '-',
        };
    }

    public function getReligionLabelAttribute(): string
    {
        return match($this->religion) {
            'islam' => 'Islam',
            'kristen' => 'Kristen',
            'katolik' => 'Katolik',
            'hindu' => 'Hindu',
            'buddha' => 'Buddha',
            'konghucu' => 'Konghucu',
            default => '-',
        };
    }

    public function getFullAddressAttribute(): string
    {
        $parts = array_filter([
            $this->address,
            $this->rt ? "RT {$this->rt}" : null,
            $this->rw ? "RW {$this->rw}" : null,
            $this->village,
            $this->district,
            $this->city,
            $this->province,
            $this->postal_code,
        ]);

        return implode(', ', $parts);
    }

    public function getBirthInfoAttribute(): string
    {
        return "{$this->birth_place}, {$this->birth_date->format('d F Y')}";
    }

    public function getAgeAttribute(): int
    {
        return $this->birth_date->age;
    }

    public function getSchoolTypeLabelAttribute(): string
    {
        return match($this->school_type) {
            'sma' => 'SMA',
            'smk' => 'SMK',
            'ma' => 'MA',
            'paket_c' => 'Paket C',
            default => '-',
        };
    }

    // ============================================
    // ACCESSORS - DOCUMENTS
    // ============================================

    public function getPhotoUrlAttribute(): ?string
    {
        return $this->photo ? Storage::url($this->photo) : null;
    }

    public function getDocumentUrls(): array
    {
        $documents = [];

        $fields = [
            'photo' => 'Pas Foto',
            'ktp_scan' => 'KTP',
            'family_card_scan' => 'Kartu Keluarga',
            'birth_certificate_scan' => 'Akta Lahir',
            'diploma_scan' => 'Ijazah/SKL',
            'transcript_scan' => 'Transkrip Nilai',
            'health_certificate_scan' => 'Surat Kesehatan',
            'kip_card_scan' => 'Kartu KIP',
        ];

        foreach ($fields as $field => $label) {
            if ($this->{$field}) {
                $documents[] = [
                    'field' => $field,
                    'label' => $label,
                    'url' => Storage::url($this->{$field}),
                    'exists' => true,
                ];
            }
        }

        // Achievement certificates (array)
        if (!empty($this->achievement_certificates)) {
            foreach ($this->achievement_certificates as $index => $cert) {
                $documents[] = [
                    'field' => "achievement_{$index}",
                    'label' => "Sertifikat Prestasi " . ($index + 1),
                    'url' => Storage::url($cert),
                    'exists' => true,
                ];
            }
        }

        return $documents;
    }

    // ============================================
    // ACCESSORS - SCORES
    // ============================================

    public function getSemesterScoresAttribute(): array
    {
        return [
            1 => $this->semester_1_avg,
            2 => $this->semester_2_avg,
            3 => $this->semester_3_avg,
            4 => $this->semester_4_avg,
            5 => $this->semester_5_avg,
        ];
    }

    public function getFormattedFinalScoreAttribute(): string
    {
        return $this->final_score ? number_format($this->final_score, 2) : '-';
    }

    // ============================================
    // ACCESSORS - PAYMENT
    // ============================================

    public function getFormattedPaymentAmountAttribute(): string
    {
        return 'Rp ' . number_format($this->payment_amount ?? 0, 0, ',', '.');
    }

    public function getFormattedUktAmountAttribute(): string
    {
        return 'Rp ' . number_format($this->ukt_amount ?? 0, 0, ',', '.');
    }

    // ============================================
    // HELPER METHODS
    // ============================================

    public function canEditForm(): bool
    {
        return in_array($this->status, ['draft', 'verification_failed']);
    }

    public function canPay(): bool
    {
        return $this->status === 'submitted' && $this->payment_status === 'unpaid';
    }

    public function canPrintExamCard(): bool
    {
        return in_array($this->status, ['verified', 'exam_scheduled', 'exam_completed', 'selection', 'announced', 'accepted', 're_registered'])
            && $this->exam_number !== null;
    }

    public function canReRegister(): bool
    {
        return $this->status === 'accepted'
            && $this->result === 'passed'
            && $this->re_registration_status !== 'completed';
    }

    public function isPassed(): bool
    {
        return $this->result === 'passed';
    }

    public function calculateFinalScore(): void
    {
        $path = $this->admissionPath;

        if (!$path || empty($path->exam_weights)) {
            return;
        }

        $weights = $path->exam_weights;
        $totalWeight = 0;
        $weightedScore = 0;

        if (isset($weights['written_exam']) && $this->written_exam_score !== null) {
            $weightedScore += ($this->written_exam_score * $weights['written_exam'] / 100);
            $totalWeight += $weights['written_exam'];
        }

        if (isset($weights['interview']) && $this->interview_score !== null) {
            $weightedScore += ($this->interview_score * $weights['interview'] / 100);
            $totalWeight += $weights['interview'];
        }

        if (isset($weights['achievement']) && $this->achievement_score !== null) {
            $weightedScore += ($this->achievement_score * $weights['achievement'] / 100);
            $totalWeight += $weights['achievement'];
        }

        if (isset($weights['report_card']) && $this->average_score !== null) {
            $weightedScore += ($this->average_score * $weights['report_card'] / 100);
            $totalWeight += $weights['report_card'];
        }

        if ($totalWeight > 0) {
            $this->final_score = round(($weightedScore / $totalWeight) * 100, 2);
        }
    }
}

Manual Review (20%):

  • ✅ Comprehensive model dengan semua relationships
  • ✅ Auto-generate registration number
  • ✅ Auto-calculate average score
  • ✅ Status labels dalam Bahasa Indonesia
  • ✅ Document URLs helper
  • ✅ Permission helpers (canPay, canPrintExamCard, etc)
  • ✅ Final score calculation dengan bobot

Update User Model

Tambahkan relationship ke Registration di User model:

// app/Models/User.php - tambahkan method ini

public function registration(): HasOne
{
    return $this->hasOne(Registration::class);
}

public function hasActiveRegistration(): bool
{
    return $this->registration()
        ->whereNotIn('status', ['cancelled', 'rejected'])
        ->exists();
}

Jalankan Migration

php artisan migrate

Status Flow Diagram

┌─────────┐
│  DRAFT  │ ← User mulai isi form
└────┬────┘
     │ Submit form
     ▼
┌───────────┐
│ SUBMITTED │ ← Menunggu pembayaran
└─────┬─────┘
      │ Bayar via Midtrans
      ▼
┌──────────┐
│   PAID   │ ← Menunggu verifikasi admin
└────┬─────┘
     │
     ├──────────────────────┐
     │ Dokumen OK           │ Dokumen bermasalah
     ▼                      ▼
┌──────────┐    ┌─────────────────────┐
│ VERIFIED │    │ VERIFICATION_FAILED │ → Kembali edit
└────┬─────┘    └─────────────────────┘
     │ Admin atur jadwal
     ▼
┌────────────────┐
│ EXAM_SCHEDULED │ ← Cetak kartu ujian
└───────┬────────┘
        │ Selesai ujian
        ▼
┌────────────────┐
│ EXAM_COMPLETED │ ← Admin input nilai
└───────┬────────┘
        │ Proses seleksi
        ▼
┌───────────┐
│ SELECTION │ ← Hitung ranking
└─────┬─────┘
      │ Pengumuman
      ▼
┌───────────┐
│ ANNOUNCED │
└─────┬─────┘
      │
      ├──────────────────────┐
      │ Lulus                │ Tidak Lulus
      ▼                      ▼
┌──────────┐           ┌──────────┐
│ ACCEPTED │           │ REJECTED │
└────┬─────┘           └──────────┘
     │ Daftar ulang + bayar UKT
     ▼
┌───────────────┐
│ RE_REGISTERED │ ← Proses selesai
└───────────────┘
💡 RECAP TABEL REGISTRATIONS:

Prompts: 3
Migration: 1 file dengan ~150 fields
Model: 1 file dengan ~500 lines

Field categories:
- Relasi: 6 foreign keys
- Data pribadi: ~15 fields
- Alamat: ~8 fields
- Orang tua: ~16 fields
- Pendidikan: ~15 fields
- Dokumen: ~9 fields
- Ujian: ~10 fields
- Status & payment: ~12 fields
- Daftar ulang: ~5 fields

Model features:
- Auto-generate registration_number
- Auto-calculate average_score
- 14 status dengan labels Indonesia
- Document URL helpers
- Permission checkers
- Final score calculation

Yang di-review manual:
- Status flow logic
- Score calculation formula
- Document handling

Di bagian selanjutnya, kita akan buat Filament Resources untuk master data — Faculty, StudyProgram, AdmissionWave, dan AdmissionPath dengan dashboard widgets untuk statistik pendaftar.

Bagian 5: Filament Admin — Master Data Resources

Sekarang kita buat admin panel untuk mengelola master data. Di bagian ini kita akan buat Resources untuk Faculty, StudyProgram, AdmissionWave, AdmissionPath, dan Dashboard widgets untuk monitoring statistik pendaftar.

Prompt 1: FacultyResource

Buatkan Filament 4 FacultyResource untuk Admin panel dengan:

Form:
- name (required)
- code (required, uppercase, auto-format)
- dean_name (nullable)
- is_active toggle

Table:
- code (badge primary)
- name (searchable)
- dean_name
- study_programs count
- is_active (icon boolean)
- created_at

Actions: edit, delete
Bulk: delete

Navigation:
- Group: Master Data
- Icon: heroicon-o-building-library
- Sort: 1

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/FacultyResource.php

namespace App\\Filament\\Admin\\Resources;

use App\\Filament\\Admin\\Resources\\FacultyResource\\Pages;
use App\\Models\\Faculty;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Support\\Str;

class FacultyResource extends Resource
{
    protected static ?string $model = Faculty::class;

    protected static ?string $navigationIcon = 'heroicon-o-building-library';
    protected static ?string $navigationGroup = 'Master Data';
    protected static ?int $navigationSort = 1;
    protected static ?string $navigationLabel = 'Fakultas';
    protected static ?string $modelLabel = 'Fakultas';
    protected static ?string $pluralModelLabel = 'Fakultas';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Informasi Fakultas')
                    ->schema([
                        Forms\\Components\\TextInput::make('name')
                            ->label('Nama Fakultas')
                            ->required()
                            ->maxLength(255)
                            ->columnSpan(2),

                        Forms\\Components\\TextInput::make('code')
                            ->label('Kode')
                            ->required()
                            ->maxLength(10)
                            ->unique(ignoreRecord: true)
                            ->dehydrateStateUsing(fn ($state) => Str::upper($state))
                            ->helperText('Contoh: FT, FEB, FIKOM'),

                        Forms\\Components\\TextInput::make('dean_name')
                            ->label('Nama Dekan')
                            ->maxLength(255)
                            ->placeholder('Dr. Nama Lengkap, M.T.'),

                        Forms\\Components\\Toggle::make('is_active')
                            ->label('Aktif')
                            ->default(true)
                            ->helperText('Fakultas tidak aktif tidak akan tampil di form pendaftaran'),
                    ])
                    ->columns(2),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('code')
                    ->label('Kode')
                    ->badge()
                    ->color('primary')
                    ->searchable()
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('name')
                    ->label('Nama Fakultas')
                    ->searchable()
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('dean_name')
                    ->label('Dekan')
                    ->searchable()
                    ->placeholder('-')
                    ->toggleable(),

                Tables\\Columns\\TextColumn::make('study_programs_count')
                    ->label('Jumlah Prodi')
                    ->counts('studyPrograms')
                    ->sortable()
                    ->alignCenter(),

                Tables\\Columns\\IconColumn::make('is_active')
                    ->label('Aktif')
                    ->boolean()
                    ->alignCenter(),

                Tables\\Columns\\TextColumn::make('created_at')
                    ->label('Dibuat')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                Tables\\Filters\\TernaryFilter::make('is_active')
                    ->label('Status Aktif'),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('code');
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListFaculties::route('/'),
            'create' => Pages\\CreateFaculty::route('/create'),
            'edit' => Pages\\EditFaculty::route('/{record}/edit'),
        ];
    }
}

Generate Pages:

mkdir -p app/Filament/Admin/Resources/FacultyResource/Pages
<?php

// app/Filament/Admin/Resources/FacultyResource/Pages/ListFaculties.php

namespace App\\Filament\\Admin\\Resources\\FacultyResource\\Pages;

use App\\Filament\\Admin\\Resources\\FacultyResource;
use Filament\\Actions;
use Filament\\Resources\\Pages\\ListRecords;

class ListFaculties extends ListRecords
{
    protected static string $resource = FacultyResource::class;

    protected function getHeaderActions(): array
    {
        return [
            Actions\\CreateAction::make(),
        ];
    }
}
<?php

// app/Filament/Admin/Resources/FacultyResource/Pages/CreateFaculty.php

namespace App\\Filament\\Admin\\Resources\\FacultyResource\\Pages;

use App\\Filament\\Admin\\Resources\\FacultyResource;
use Filament\\Resources\\Pages\\CreateRecord;

class CreateFaculty extends CreateRecord
{
    protected static string $resource = FacultyResource::class;

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }
}
<?php

// app/Filament/Admin/Resources/FacultyResource/Pages/EditFaculty.php

namespace App\\Filament\\Admin\\Resources\\FacultyResource\\Pages;

use App\\Filament\\Admin\\Resources\\FacultyResource;
use Filament\\Actions;
use Filament\\Resources\\Pages\\EditRecord;

class EditFaculty extends EditRecord
{
    protected static string $resource = FacultyResource::class;

    protected function getHeaderActions(): array
    {
        return [
            Actions\\DeleteAction::make(),
        ];
    }
}

Manual Review (20%):

  • ✅ Labels dalam Bahasa Indonesia
  • ✅ Code auto-uppercase
  • ✅ Study programs count
  • Tidak perlu edit

Prompt 2: StudyProgramResource

Buatkan StudyProgramResource dengan:

Form:
- faculty_id (select relationship, searchable)
- name, code
- degree (select: D3, D4, S1, S2, S3)
- quota (numeric)
- accreditation (select dengan format baru)
- tuition_fee (currency format Rp)
- description (textarea)
- is_active toggle

Table:
- faculty.name (badge)
- code + name combined
- degree badge
- accreditation badge dengan warna
- quota dengan progress bar (filled/quota)
- formatted tuition
- is_active

Filter: by faculty, by degree, by accreditation

Navigation:
- Group: Master Data
- Icon: heroicon-o-academic-cap
- Sort: 2

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/StudyProgramResource.php

namespace App\\Filament\\Admin\\Resources;

use App\\Filament\\Admin\\Resources\\StudyProgramResource\\Pages;
use App\\Models\\StudyProgram;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Support\\Str;

class StudyProgramResource extends Resource
{
    protected static ?string $model = StudyProgram::class;

    protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
    protected static ?string $navigationGroup = 'Master Data';
    protected static ?int $navigationSort = 2;
    protected static ?string $navigationLabel = 'Program Studi';
    protected static ?string $modelLabel = 'Program Studi';
    protected static ?string $pluralModelLabel = 'Program Studi';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Informasi Program Studi')
                    ->schema([
                        Forms\\Components\\Select::make('faculty_id')
                            ->label('Fakultas')
                            ->relationship('faculty', 'name')
                            ->searchable()
                            ->preload()
                            ->required(),

                        Forms\\Components\\TextInput::make('name')
                            ->label('Nama Program Studi')
                            ->required()
                            ->maxLength(255),

                        Forms\\Components\\TextInput::make('code')
                            ->label('Kode')
                            ->required()
                            ->maxLength(10)
                            ->dehydrateStateUsing(fn ($state) => Str::upper($state)),

                        Forms\\Components\\Select::make('degree')
                            ->label('Jenjang')
                            ->options([
                                'D3' => 'D3 (Diploma)',
                                'D4' => 'D4 (Sarjana Terapan)',
                                'S1' => 'S1 (Sarjana)',
                                'S2' => 'S2 (Magister)',
                                'S3' => 'S3 (Doktor)',
                            ])
                            ->required()
                            ->default('S1'),
                    ])
                    ->columns(2),

                Forms\\Components\\Section::make('Kuota & Akreditasi')
                    ->schema([
                        Forms\\Components\\TextInput::make('quota')
                            ->label('Kuota Mahasiswa')
                            ->numeric()
                            ->default(0)
                            ->minValue(0)
                            ->suffix('orang'),

                        Forms\\Components\\Select::make('accreditation')
                            ->label('Akreditasi')
                            ->options([
                                'Unggul' => 'Unggul',
                                'Baik Sekali' => 'Baik Sekali',
                                'Baik' => 'Baik',
                                'A' => 'A',
                                'B' => 'B',
                                'C' => 'C',
                            ])
                            ->placeholder('Pilih akreditasi'),

                        Forms\\Components\\TextInput::make('tuition_fee')
                            ->label('UKT per Semester')
                            ->numeric()
                            ->prefix('Rp')
                            ->default(0)
                            ->minValue(0),

                        Forms\\Components\\Toggle::make('is_active')
                            ->label('Aktif')
                            ->default(true),
                    ])
                    ->columns(2),

                Forms\\Components\\Section::make('Deskripsi')
                    ->schema([
                        Forms\\Components\\Textarea::make('description')
                            ->label('Deskripsi Program Studi')
                            ->rows(4)
                            ->columnSpanFull(),
                    ])
                    ->collapsible(),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('faculty.code')
                    ->label('Fakultas')
                    ->badge()
                    ->color('info')
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('code')
                    ->label('Kode')
                    ->searchable()
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('name')
                    ->label('Nama Program Studi')
                    ->searchable()
                    ->sortable()
                    ->wrap(),

                Tables\\Columns\\TextColumn::make('degree')
                    ->label('Jenjang')
                    ->badge()
                    ->color('primary'),

                Tables\\Columns\\TextColumn::make('accreditation')
                    ->label('Akreditasi')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'A', 'Unggul' => 'success',
                        'B', 'Baik Sekali' => 'info',
                        'C', 'Baik' => 'warning',
                        default => 'gray',
                    })
                    ->placeholder('-'),

                Tables\\Columns\\TextColumn::make('quota')
                    ->label('Kuota')
                    ->numeric()
                    ->alignCenter(),

                Tables\\Columns\\TextColumn::make('formatted_tuition')
                    ->label('UKT')
                    ->sortable(query: fn ($query, $direction) =>
                        $query->orderBy('tuition_fee', $direction)
                    ),

                Tables\\Columns\\IconColumn::make('is_active')
                    ->label('Aktif')
                    ->boolean()
                    ->alignCenter(),
            ])
            ->filters([
                Tables\\Filters\\SelectFilter::make('faculty')
                    ->relationship('faculty', 'name')
                    ->label('Fakultas')
                    ->searchable()
                    ->preload(),

                Tables\\Filters\\SelectFilter::make('degree')
                    ->label('Jenjang')
                    ->options([
                        'D3' => 'D3',
                        'D4' => 'D4',
                        'S1' => 'S1',
                        'S2' => 'S2',
                        'S3' => 'S3',
                    ]),

                Tables\\Filters\\SelectFilter::make('accreditation')
                    ->label('Akreditasi')
                    ->options([
                        'Unggul' => 'Unggul',
                        'Baik Sekali' => 'Baik Sekali',
                        'Baik' => 'Baik',
                        'A' => 'A',
                        'B' => 'B',
                        'C' => 'C',
                    ]),

                Tables\\Filters\\TernaryFilter::make('is_active')
                    ->label('Status Aktif'),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('faculty.code');
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListStudyPrograms::route('/'),
            'create' => Pages\\CreateStudyProgram::route('/create'),
            'edit' => Pages\\EditStudyProgram::route('/{record}/edit'),
        ];
    }
}

Manual Review (20%):

  • ✅ Accreditation colors sesuai peringkat
  • ✅ UKT dengan prefix Rp
  • ✅ Filters lengkap
  • Tidak perlu edit

Prompt 3: AdmissionWaveResource

Buatkan AdmissionWaveResource dengan:

Form:
- name, year
- Tanggal-tanggal: registration_start/end, exam_date,
  announcement_date, re_registration_deadline
- status (select dengan visual)
- is_active toggle

Table:
- name + year combined
- registration period
- exam_date
- announcement_date
- status badge dengan warna
- registrations count
- is_active

Tampilkan juga jadwal ujian sebagai relation manager.

Navigation:
- Group: Master Data
- Icon: heroicon-o-calendar-days
- Sort: 3

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/AdmissionWaveResource.php

namespace App\\Filament\\Admin\\Resources;

use App\\Filament\\Admin\\Resources\\AdmissionWaveResource\\Pages;
use App\\Filament\\Admin\\Resources\\AdmissionWaveResource\\RelationManagers;
use App\\Models\\AdmissionWave;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;

class AdmissionWaveResource extends Resource
{
    protected static ?string $model = AdmissionWave::class;

    protected static ?string $navigationIcon = 'heroicon-o-calendar-days';
    protected static ?string $navigationGroup = 'Master Data';
    protected static ?int $navigationSort = 3;
    protected static ?string $navigationLabel = 'Gelombang Pendaftaran';
    protected static ?string $modelLabel = 'Gelombang';
    protected static ?string $pluralModelLabel = 'Gelombang Pendaftaran';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Informasi Gelombang')
                    ->schema([
                        Forms\\Components\\TextInput::make('name')
                            ->label('Nama Gelombang')
                            ->required()
                            ->maxLength(255)
                            ->placeholder('Gelombang 1'),

                        Forms\\Components\\TextInput::make('year')
                            ->label('Tahun Akademik')
                            ->numeric()
                            ->required()
                            ->default(date('Y'))
                            ->minValue(2020)
                            ->maxValue(2100),

                        Forms\\Components\\Select::make('status')
                            ->label('Status')
                            ->options([
                                'upcoming' => '🕐 Akan Datang',
                                'open' => '✅ Dibuka',
                                'closed' => '🔒 Ditutup',
                                'completed' => '✔️ Selesai',
                            ])
                            ->required()
                            ->default('upcoming'),

                        Forms\\Components\\Toggle::make('is_active')
                            ->label('Aktif')
                            ->default(true),
                    ])
                    ->columns(2),

                Forms\\Components\\Section::make('Jadwal Penting')
                    ->schema([
                        Forms\\Components\\DatePicker::make('registration_start_date')
                            ->label('Tanggal Buka Pendaftaran')
                            ->required()
                            ->native(false)
                            ->displayFormat('d F Y'),

                        Forms\\Components\\DatePicker::make('registration_end_date')
                            ->label('Tanggal Tutup Pendaftaran')
                            ->required()
                            ->native(false)
                            ->displayFormat('d F Y')
                            ->afterOrEqual('registration_start_date'),

                        Forms\\Components\\DatePicker::make('exam_date')
                            ->label('Tanggal Ujian')
                            ->native(false)
                            ->displayFormat('d F Y')
                            ->afterOrEqual('registration_end_date'),

                        Forms\\Components\\DatePicker::make('announcement_date')
                            ->label('Tanggal Pengumuman')
                            ->native(false)
                            ->displayFormat('d F Y')
                            ->afterOrEqual('exam_date'),

                        Forms\\Components\\DatePicker::make('re_registration_deadline')
                            ->label('Batas Daftar Ulang')
                            ->native(false)
                            ->displayFormat('d F Y')
                            ->afterOrEqual('announcement_date'),
                    ])
                    ->columns(2)
                    ->description('Pastikan tanggal berurutan: Pendaftaran → Ujian → Pengumuman → Daftar Ulang'),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('full_name')
                    ->label('Gelombang')
                    ->searchable(['name', 'year'])
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('registration_period')
                    ->label('Periode Pendaftaran'),

                Tables\\Columns\\TextColumn::make('exam_date')
                    ->label('Ujian')
                    ->date('d M Y')
                    ->placeholder('-'),

                Tables\\Columns\\TextColumn::make('announcement_date')
                    ->label('Pengumuman')
                    ->date('d M Y')
                    ->placeholder('-'),

                Tables\\Columns\\TextColumn::make('status')
                    ->label('Status')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'upcoming' => 'gray',
                        'open' => 'success',
                        'closed' => 'warning',
                        'completed' => 'info',
                        default => 'gray',
                    })
                    ->formatStateUsing(fn (string $state): string => match ($state) {
                        'upcoming' => 'Akan Datang',
                        'open' => 'Dibuka',
                        'closed' => 'Ditutup',
                        'completed' => 'Selesai',
                        default => $state,
                    }),

                Tables\\Columns\\TextColumn::make('registrations_count')
                    ->label('Pendaftar')
                    ->counts('registrations')
                    ->alignCenter(),

                Tables\\Columns\\IconColumn::make('is_active')
                    ->label('Aktif')
                    ->boolean()
                    ->alignCenter(),
            ])
            ->filters([
                Tables\\Filters\\SelectFilter::make('year')
                    ->label('Tahun')
                    ->options(fn () => AdmissionWave::distinct()->pluck('year', 'year')->toArray()),

                Tables\\Filters\\SelectFilter::make('status')
                    ->label('Status')
                    ->options([
                        'upcoming' => 'Akan Datang',
                        'open' => 'Dibuka',
                        'closed' => 'Ditutup',
                        'completed' => 'Selesai',
                    ]),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make()
                    ->before(function (AdmissionWave $record) {
                        if ($record->registrations()->exists()) {
                            throw new \\Exception('Tidak dapat menghapus gelombang yang sudah memiliki pendaftar.');
                        }
                    }),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('year', 'desc');
    }

    public static function getRelations(): array
    {
        return [
            RelationManagers\\ExamSchedulesRelationManager::class,
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListAdmissionWaves::route('/'),
            'create' => Pages\\CreateAdmissionWave::route('/create'),
            'edit' => Pages\\EditAdmissionWave::route('/{record}/edit'),
        ];
    }
}

ExamSchedules Relation Manager:

<?php

// app/Filament/Admin/Resources/AdmissionWaveResource/RelationManagers/ExamSchedulesRelationManager.php

namespace App\\Filament\\Admin\\Resources\\AdmissionWaveResource\\RelationManagers;

use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\RelationManagers\\RelationManager;
use Filament\\Tables;
use Filament\\Tables\\Table;

class ExamSchedulesRelationManager extends RelationManager
{
    protected static string $relationship = 'examSchedules';
    protected static ?string $title = 'Jadwal Ujian';
    protected static ?string $modelLabel = 'Jadwal';

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Select::make('exam_type')
                    ->label('Jenis Ujian')
                    ->options([
                        'written' => 'Ujian Tulis',
                        'interview' => 'Wawancara',
                        'practical' => 'Ujian Praktik',
                        'health_check' => 'Tes Kesehatan',
                    ])
                    ->required(),

                Forms\\Components\\DatePicker::make('exam_date')
                    ->label('Tanggal')
                    ->required()
                    ->native(false),

                Forms\\Components\\TimePicker::make('start_time')
                    ->label('Jam Mulai')
                    ->required()
                    ->seconds(false),

                Forms\\Components\\TimePicker::make('end_time')
                    ->label('Jam Selesai')
                    ->required()
                    ->seconds(false)
                    ->after('start_time'),

                Forms\\Components\\Repeater::make('rooms')
                    ->label('Ruangan')
                    ->schema([
                        Forms\\Components\\TextInput::make('room')
                            ->label('Nama Ruangan')
                            ->required(),
                        Forms\\Components\\TextInput::make('capacity')
                            ->label('Kapasitas')
                            ->numeric()
                            ->required()
                            ->minValue(1),
                    ])
                    ->columns(2)
                    ->columnSpanFull()
                    ->defaultItems(1),

                Forms\\Components\\Textarea::make('notes')
                    ->label('Catatan')
                    ->columnSpanFull(),
            ]);
    }

    public function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('exam_type_label')
                    ->label('Jenis Ujian'),

                Tables\\Columns\\TextColumn::make('exam_date')
                    ->label('Tanggal')
                    ->date('d M Y'),

                Tables\\Columns\\TextColumn::make('time_range')
                    ->label('Waktu'),

                Tables\\Columns\\TextColumn::make('total_capacity')
                    ->label('Kapasitas')
                    ->suffix(' orang'),
            ])
            ->headerActions([
                Tables\\Actions\\CreateAction::make(),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make(),
            ]);
    }
}

Manual Review (20%):

  • ✅ Date validations (afterOrEqual)
  • ✅ Status dengan emoji visual
  • ✅ Delete protection jika ada pendaftar
  • ✅ Repeater untuk rooms

Prompt 4: AdmissionPathResource

Buatkan AdmissionPathResource dengan:

Form:
- name, code
- registration_fee (currency Rp)
- requirements (repeater untuk list syarat)
- exam_weights (key-value repeater untuk bobot)
- description (rich editor)
- is_active

Table:
- code badge
- name
- formatted fee (gratis jika 0)
- requirements count
- is_active

Navigation:
- Group: Master Data
- Icon: heroicon-o-arrow-path
- Sort: 4

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/AdmissionPathResource.php

namespace App\\Filament\\Admin\\Resources;

use App\\Filament\\Admin\\Resources\\AdmissionPathResource\\Pages;
use App\\Models\\AdmissionPath;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Support\\Str;

class AdmissionPathResource extends Resource
{
    protected static ?string $model = AdmissionPath::class;

    protected static ?string $navigationIcon = 'heroicon-o-arrow-path';
    protected static ?string $navigationGroup = 'Master Data';
    protected static ?int $navigationSort = 4;
    protected static ?string $navigationLabel = 'Jalur Pendaftaran';
    protected static ?string $modelLabel = 'Jalur';
    protected static ?string $pluralModelLabel = 'Jalur Pendaftaran';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Informasi Jalur')
                    ->schema([
                        Forms\\Components\\TextInput::make('name')
                            ->label('Nama Jalur')
                            ->required()
                            ->maxLength(255)
                            ->placeholder('Jalur Reguler'),

                        Forms\\Components\\TextInput::make('code')
                            ->label('Kode')
                            ->required()
                            ->maxLength(20)
                            ->unique(ignoreRecord: true)
                            ->dehydrateStateUsing(fn ($state) => Str::upper($state))
                            ->helperText('Contoh: REG, PRESTASI, KIP'),

                        Forms\\Components\\TextInput::make('registration_fee')
                            ->label('Biaya Pendaftaran')
                            ->numeric()
                            ->prefix('Rp')
                            ->default(0)
                            ->minValue(0)
                            ->helperText('Isi 0 untuk gratis (misal KIP-Kuliah)'),

                        Forms\\Components\\Toggle::make('is_active')
                            ->label('Aktif')
                            ->default(true),
                    ])
                    ->columns(2),

                Forms\\Components\\Section::make('Persyaratan')
                    ->schema([
                        Forms\\Components\\Repeater::make('requirements')
                            ->label('Daftar Persyaratan')
                            ->simple(
                                Forms\\Components\\TextInput::make('requirement')
                                    ->required()
                                    ->placeholder('Masukkan persyaratan')
                            )
                            ->defaultItems(3)
                            ->addActionLabel('Tambah Persyaratan')
                            ->columnSpanFull(),
                    ]),

                Forms\\Components\\Section::make('Bobot Penilaian')
                    ->schema([
                        Forms\\Components\\KeyValue::make('exam_weights')
                            ->label('Bobot Nilai')
                            ->keyLabel('Komponen')
                            ->valueLabel('Bobot (%)')
                            ->keyPlaceholder('Contoh: written_exam')
                            ->valuePlaceholder('Contoh: 40')
                            ->columnSpanFull()
                            ->helperText('Total bobot harus 100%. Keys: written_exam, interview, achievement, report_card'),
                    ])
                    ->collapsible(),

                Forms\\Components\\Section::make('Deskripsi')
                    ->schema([
                        Forms\\Components\\RichEditor::make('description')
                            ->label('Deskripsi Jalur')
                            ->columnSpanFull(),
                    ])
                    ->collapsible(),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('code')
                    ->label('Kode')
                    ->badge()
                    ->color('primary')
                    ->searchable(),

                Tables\\Columns\\TextColumn::make('name')
                    ->label('Nama Jalur')
                    ->searchable()
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('formatted_fee')
                    ->label('Biaya')
                    ->badge()
                    ->color(fn (AdmissionPath $record): string =>
                        $record->registration_fee === 0 ? 'success' : 'warning'
                    ),

                Tables\\Columns\\TextColumn::make('requirements')
                    ->label('Persyaratan')
                    ->formatStateUsing(fn ($state) => count($state ?? []) . ' syarat')
                    ->alignCenter(),

                Tables\\Columns\\IconColumn::make('is_active')
                    ->label('Aktif')
                    ->boolean()
                    ->alignCenter(),
            ])
            ->filters([
                Tables\\Filters\\TernaryFilter::make('is_active')
                    ->label('Status Aktif'),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ]);
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListAdmissionPaths::route('/'),
            'create' => Pages\\CreateAdmissionPath::route('/create'),
            'edit' => Pages\\EditAdmissionPath::route('/{record}/edit'),
        ];
    }
}

Manual Review (20%):

  • ✅ Simple repeater untuk requirements
  • ✅ KeyValue untuk exam_weights
  • ✅ Fee gratis dengan badge hijau
  • Tidak perlu edit

Prompt 5: Dashboard Widgets

Buatkan dashboard widgets untuk Admin panel:

1. StatsOverview - 4 stats:
   - Total Pendaftar (semua gelombang aktif)
   - Menunggu Verifikasi
   - Sudah Bayar
   - Diterima

2. RegistrationChart - Line chart pendaftaran per hari (30 hari terakhir)

3. PathDistributionChart - Pie chart distribusi per jalur

4. LatestRegistrations - Table 5 pendaftar terbaru

Response Claude (80%):

<?php

// app/Filament/Admin/Widgets/StatsOverview.php

namespace App\\Filament\\Admin\\Widgets;

use App\\Models\\Registration;
use App\\Models\\AdmissionWave;
use Filament\\Widgets\\StatsOverviewWidget as BaseWidget;
use Filament\\Widgets\\StatsOverviewWidget\\Stat;

class StatsOverview extends BaseWidget
{
    protected static ?int $sort = 1;

    protected function getStats(): array
    {
        $activeWaveIds = AdmissionWave::where('is_active', true)->pluck('id');

        $total = Registration::whereIn('admission_wave_id', $activeWaveIds)
            ->whereNotIn('status', ['draft', 'cancelled'])
            ->count();

        $pendingVerification = Registration::whereIn('admission_wave_id', $activeWaveIds)
            ->where('status', 'paid')
            ->count();

        $paid = Registration::whereIn('admission_wave_id', $activeWaveIds)
            ->where('payment_status', 'paid')
            ->count();

        $accepted = Registration::whereIn('admission_wave_id', $activeWaveIds)
            ->whereIn('status', ['accepted', 're_registered'])
            ->count();

        return [
            Stat::make('Total Pendaftar', number_format($total))
                ->description('Gelombang aktif')
                ->icon('heroicon-o-users')
                ->color('primary'),

            Stat::make('Menunggu Verifikasi', number_format($pendingVerification))
                ->description('Perlu review dokumen')
                ->icon('heroicon-o-clock')
                ->color($pendingVerification > 0 ? 'warning' : 'success'),

            Stat::make('Sudah Bayar', number_format($paid))
                ->description('Pembayaran confirmed')
                ->icon('heroicon-o-banknotes')
                ->color('success'),

            Stat::make('Diterima', number_format($accepted))
                ->description('Lulus seleksi')
                ->icon('heroicon-o-academic-cap')
                ->color('info'),
        ];
    }
}
<?php

// app/Filament/Admin/Widgets/RegistrationChart.php

namespace App\\Filament\\Admin\\Widgets;

use App\\Models\\Registration;
use Filament\\Widgets\\ChartWidget;
use Illuminate\\Support\\Carbon;

class RegistrationChart extends ChartWidget
{
    protected static ?string $heading = 'Pendaftaran 30 Hari Terakhir';
    protected static ?int $sort = 2;
    protected int | string | array $columnSpan = 'full';

    protected function getData(): array
    {
        $data = [];
        $labels = [];

        for ($i = 29; $i >= 0; $i--) {
            $date = Carbon::now()->subDays($i);
            $labels[] = $date->format('d M');

            $count = Registration::whereDate('created_at', $date)
                ->whereNotIn('status', ['draft', 'cancelled'])
                ->count();

            $data[] = $count;
        }

        return [
            'datasets' => [
                [
                    'label' => 'Pendaftar',
                    'data' => $data,
                    'fill' => true,
                    'backgroundColor' => 'rgba(16, 185, 129, 0.2)',
                    'borderColor' => 'rgb(16, 185, 129)',
                ],
            ],
            'labels' => $labels,
        ];
    }

    protected function getType(): string
    {
        return 'line';
    }
}
<?php

// app/Filament/Admin/Widgets/PathDistributionChart.php

namespace App\\Filament\\Admin\\Widgets;

use App\\Models\\Registration;
use App\\Models\\AdmissionPath;
use App\\Models\\AdmissionWave;
use Filament\\Widgets\\ChartWidget;

class PathDistributionChart extends ChartWidget
{
    protected static ?string $heading = 'Distribusi per Jalur';
    protected static ?int $sort = 3;

    protected function getData(): array
    {
        $activeWaveIds = AdmissionWave::where('is_active', true)->pluck('id');

        $paths = AdmissionPath::withCount(['registrations' => function ($query) use ($activeWaveIds) {
            $query->whereIn('admission_wave_id', $activeWaveIds)
                  ->whereNotIn('status', ['draft', 'cancelled']);
        }])->get();

        $colors = [
            'rgba(59, 130, 246, 0.8)',   // blue
            'rgba(16, 185, 129, 0.8)',   // green
            'rgba(245, 158, 11, 0.8)',   // amber
            'rgba(239, 68, 68, 0.8)',    // red
            'rgba(139, 92, 246, 0.8)',   // purple
        ];

        return [
            'datasets' => [
                [
                    'data' => $paths->pluck('registrations_count')->toArray(),
                    'backgroundColor' => array_slice($colors, 0, $paths->count()),
                ],
            ],
            'labels' => $paths->pluck('name')->toArray(),
        ];
    }

    protected function getType(): string
    {
        return 'pie';
    }
}
<?php

// app/Filament/Admin/Widgets/LatestRegistrations.php

namespace App\\Filament\\Admin\\Widgets;

use App\\Models\\Registration;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Filament\\Widgets\\TableWidget as BaseWidget;

class LatestRegistrations extends BaseWidget
{
    protected static ?string $heading = 'Pendaftar Terbaru';
    protected static ?int $sort = 4;
    protected int | string | array $columnSpan = 'full';

    public function table(Table $table): Table
    {
        return $table
            ->query(
                Registration::query()
                    ->whereNotIn('status', ['draft', 'cancelled'])
                    ->latest()
                    ->limit(5)
            )
            ->columns([
                Tables\\Columns\\TextColumn::make('registration_number')
                    ->label('No. Pendaftaran')
                    ->searchable(),

                Tables\\Columns\\TextColumn::make('full_name')
                    ->label('Nama')
                    ->searchable(),

                Tables\\Columns\\TextColumn::make('admissionPath.name')
                    ->label('Jalur')
                    ->badge(),

                Tables\\Columns\\TextColumn::make('firstChoice.name')
                    ->label('Pilihan 1')
                    ->limit(20),

                Tables\\Columns\\TextColumn::make('status')
                    ->label('Status')
                    ->badge()
                    ->color(fn (Registration $record): string => $record->status_color)
                    ->formatStateUsing(fn (Registration $record): string => $record->status_label),

                Tables\\Columns\\TextColumn::make('created_at')
                    ->label('Tanggal')
                    ->dateTime('d M Y H:i'),
            ])
            ->paginated(false);
    }
}

Register widgets di AdminPanelProvider:

// Di AdminPanelProvider, pastikan widgets ter-discover

->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\\\Filament\\\\Admin\\\\Widgets')

Manual Review (20%):

  • ✅ Stats dengan conditional colors
  • ✅ Line chart 30 hari
  • ✅ Pie chart distribusi jalur
  • ✅ Latest registrations table
💡 RECAP FILAMENT ADMIN - MASTER DATA:

Prompts: 5
Resources created: 4
- FacultyResource
- StudyProgramResource
- AdmissionWaveResource (+ ExamSchedules RelationManager)
- AdmissionPathResource

Widgets created: 4
- StatsOverview
- RegistrationChart
- PathDistributionChart
- LatestRegistrations

Features:
- Labels Bahasa Indonesia
- Badge colors berdasarkan status/nilai
- Date validations
- Delete protection
- Relation managers
- Dashboard charts

Yang di-review manual:
- Akreditasi color mapping
- Status emojis
- Delete validation

Di bagian selanjutnya, kita akan buat RegistrationResource untuk admin mengelola pendaftar — verifikasi dokumen, input nilai, dan generate pengumuman.

Bagian 6: Filament Admin — Registration Management

Ini adalah bagian paling kompleks dari admin panel — mengelola pendaftar. Admin perlu bisa melihat semua data pendaftar, verifikasi dokumen, input nilai ujian, dan manage status pendaftaran.

Prompt 1: RegistrationResource — Table View

Buatkan RegistrationResource untuk Admin panel dengan:

Table columns:
- registration_number (searchable, copyable)
- photo (circular image)
- full_name (searchable)
- admissionPath.name (badge)
- firstChoice.name (limit 20)
- status (badge dengan warna dari model)
- payment_status (badge)
- created_at

Filters:
- admission_wave (select)
- admission_path (select)
- status (select)
- payment_status (select)
- first_choice program (select)

TIDAK bisa create/edit dari admin (hanya view dan actions).

Navigation:
- Group: Pendaftaran
- Icon: heroicon-o-document-text
- Sort: 1
- Badge: jumlah pending verification

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/RegistrationResource.php

namespace App\\Filament\\Admin\\Resources;

use App\\Filament\\Admin\\Resources\\RegistrationResource\\Pages;
use App\\Models\\Registration;
use App\\Models\\AdmissionWave;
use App\\Models\\AdmissionPath;
use App\\Models\\StudyProgram;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Infolists;
use Filament\\Infolists\\Infolist;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Database\\Eloquent\\Builder;

class RegistrationResource extends Resource
{
    protected static ?string $model = Registration::class;

    protected static ?string $navigationIcon = 'heroicon-o-document-text';
    protected static ?string $navigationGroup = 'Pendaftaran';
    protected static ?int $navigationSort = 1;
    protected static ?string $navigationLabel = 'Data Pendaftar';
    protected static ?string $modelLabel = 'Pendaftar';
    protected static ?string $pluralModelLabel = 'Data Pendaftar';

    public static function getNavigationBadge(): ?string
    {
        $count = static::getModel()::where('status', 'paid')->count();
        return $count > 0 ? (string) $count : null;
    }

    public static function getNavigationBadgeColor(): ?string
    {
        return 'warning';
    }

    public static function canCreate(): bool
    {
        return false;
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('registration_number')
                    ->label('No. Registrasi')
                    ->searchable()
                    ->copyable()
                    ->copyMessage('Nomor registrasi disalin!')
                    ->sortable()
                    ->weight('bold'),

                Tables\\Columns\\ImageColumn::make('photo')
                    ->label('Foto')
                    ->circular()
                    ->defaultImageUrl(asset('images/default-avatar.png')),

                Tables\\Columns\\TextColumn::make('full_name')
                    ->label('Nama Lengkap')
                    ->searchable()
                    ->sortable()
                    ->limit(25),

                Tables\\Columns\\TextColumn::make('admissionWave.full_name')
                    ->label('Gelombang')
                    ->badge()
                    ->color('info')
                    ->toggleable(),

                Tables\\Columns\\TextColumn::make('admissionPath.code')
                    ->label('Jalur')
                    ->badge()
                    ->color('primary'),

                Tables\\Columns\\TextColumn::make('firstChoice.name')
                    ->label('Pilihan 1')
                    ->limit(20)
                    ->tooltip(fn (Registration $record): string => $record->firstChoice?->name ?? '-'),

                Tables\\Columns\\TextColumn::make('status')
                    ->label('Status')
                    ->badge()
                    ->color(fn (Registration $record): string => $record->status_color)
                    ->formatStateUsing(fn (Registration $record): string => $record->status_label),

                Tables\\Columns\\TextColumn::make('payment_status')
                    ->label('Pembayaran')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'paid' => 'success',
                        'pending' => 'warning',
                        'unpaid' => 'danger',
                        default => 'gray',
                    })
                    ->formatStateUsing(fn (Registration $record): string => $record->payment_status_label),

                Tables\\Columns\\TextColumn::make('created_at')
                    ->label('Tanggal Daftar')
                    ->dateTime('d M Y H:i')
                    ->sortable()
                    ->toggleable(),
            ])
            ->filters([
                Tables\\Filters\\SelectFilter::make('admission_wave_id')
                    ->label('Gelombang')
                    ->options(fn () => AdmissionWave::where('is_active', true)->pluck('name', 'id'))
                    ->searchable(),

                Tables\\Filters\\SelectFilter::make('admission_path_id')
                    ->label('Jalur')
                    ->options(fn () => AdmissionPath::where('is_active', true)->pluck('name', 'id')),

                Tables\\Filters\\SelectFilter::make('status')
                    ->label('Status')
                    ->options([
                        'draft' => 'Draft',
                        'submitted' => 'Menunggu Pembayaran',
                        'paid' => 'Menunggu Verifikasi',
                        'verification_failed' => 'Verifikasi Gagal',
                        'verified' => 'Terverifikasi',
                        'exam_scheduled' => 'Ujian Dijadwalkan',
                        'exam_completed' => 'Ujian Selesai',
                        'selection' => 'Proses Seleksi',
                        'announced' => 'Diumumkan',
                        'accepted' => 'Diterima',
                        'rejected' => 'Tidak Diterima',
                        're_registered' => 'Sudah Daftar Ulang',
                    ]),

                Tables\\Filters\\SelectFilter::make('payment_status')
                    ->label('Pembayaran')
                    ->options([
                        'unpaid' => 'Belum Bayar',
                        'pending' => 'Pending',
                        'paid' => 'Sudah Bayar',
                    ]),

                Tables\\Filters\\SelectFilter::make('first_choice_id')
                    ->label('Pilihan 1')
                    ->options(fn () => StudyProgram::where('is_active', true)->pluck('name', 'id'))
                    ->searchable(),
            ])
            ->actions([
                Tables\\Actions\\ViewAction::make(),
                Tables\\Actions\\ActionGroup::make([
                    Tables\\Actions\\Action::make('verify')
                        ->label('Verifikasi')
                        ->icon('heroicon-o-check-circle')
                        ->color('success')
                        ->visible(fn (Registration $record): bool => $record->status === 'paid')
                        ->url(fn (Registration $record): string =>
                            static::getUrl('verify', ['record' => $record])
                        ),
                    Tables\\Actions\\Action::make('inputScore')
                        ->label('Input Nilai')
                        ->icon('heroicon-o-pencil-square')
                        ->color('warning')
                        ->visible(fn (Registration $record): bool =>
                            in_array($record->status, ['exam_completed', 'selection'])
                        )
                        ->url(fn (Registration $record): string =>
                            static::getUrl('score', ['record' => $record])
                        ),
                ]),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\BulkAction::make('bulkVerify')
                        ->label('Verifikasi Terpilih')
                        ->icon('heroicon-o-check-circle')
                        ->color('success')
                        ->requiresConfirmation()
                        ->action(function ($records) {
                            $records->each(function ($record) {
                                if ($record->status === 'paid') {
                                    $record->update([
                                        'status' => 'verified',
                                        'verified_at' => now(),
                                        'verified_by' => auth()->id(),
                                    ]);
                                }
                            });
                        }),

                    Tables\\Actions\\BulkAction::make('exportExcel')
                        ->label('Export Excel')
                        ->icon('heroicon-o-arrow-down-tray')
                        ->action(function ($records) {
                            // Implement export logic
                            return response()->streamDownload(function () use ($records) {
                                // Excel export implementation
                            }, 'pendaftar.xlsx');
                        }),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }

    public static function getRelations(): array
    {
        return [];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListRegistrations::route('/'),
            'view' => Pages\\ViewRegistration::route('/{record}'),
            'verify' => Pages\\VerifyRegistration::route('/{record}/verify'),
            'score' => Pages\\InputScore::route('/{record}/score'),
        ];
    }
}

Manual Review (20%):

  • ✅ Navigation badge untuk pending verification
  • ✅ canCreate() = false
  • ✅ Status/payment colors dari model
  • ✅ Action groups untuk clean UI
  • ✅ Bulk verify action

Prompt 2: ViewRegistration Page — Infolist

Buatkan ViewRegistration page dengan Infolist yang menampilkan:

Section 1 - Status & Registrasi:
- Status badge, payment status, registration number
- Gelombang, jalur, pilihan prodi 1 & 2

Section 2 - Data Pribadi:
- Foto (besar), NIK, nama, TTL, gender, agama
- Alamat lengkap, telepon

Section 3 - Data Orang Tua:
- Data ayah (nama, pendidikan, pekerjaan, penghasilan)
- Data ibu (sama)
- Data wali jika ada

Section 4 - Data Pendidikan:
- Info sekolah
- Nilai rapor per semester dalam grid

Section 5 - Dokumen:
- Tampilkan semua dokumen yang diupload dengan preview

Section 6 - Ujian & Seleksi:
- Nomor peserta, ruangan
- Nilai-nilai ujian
- Final score, ranking, result

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/RegistrationResource/Pages/ViewRegistration.php

namespace App\\Filament\\Admin\\Resources\\RegistrationResource\\Pages;

use App\\Filament\\Admin\\Resources\\RegistrationResource;
use App\\Models\\Registration;
use Filament\\Actions;
use Filament\\Infolists\\Components;
use Filament\\Infolists\\Infolist;
use Filament\\Resources\\Pages\\ViewRecord;
use Illuminate\\Support\\Facades\\Storage;

class ViewRegistration extends ViewRecord
{
    protected static string $resource = RegistrationResource::class;

    protected function getHeaderActions(): array
    {
        return [
            Actions\\Action::make('verify')
                ->label('Verifikasi Dokumen')
                ->icon('heroicon-o-check-circle')
                ->color('success')
                ->visible(fn (): bool => $this->record->status === 'paid')
                ->url(fn (): string => RegistrationResource::getUrl('verify', ['record' => $this->record])),

            Actions\\Action::make('inputScore')
                ->label('Input Nilai')
                ->icon('heroicon-o-pencil-square')
                ->color('warning')
                ->visible(fn (): bool => in_array($this->record->status, ['exam_completed', 'selection']))
                ->url(fn (): string => RegistrationResource::getUrl('score', ['record' => $this->record])),

            Actions\\Action::make('printExamCard')
                ->label('Cetak Kartu Ujian')
                ->icon('heroicon-o-printer')
                ->color('info')
                ->visible(fn (): bool => $this->record->canPrintExamCard())
                ->url(fn (): string => route('admin.exam-card.print', $this->record))
                ->openUrlInNewTab(),
        ];
    }

    public function infolist(Infolist $infolist): Infolist
    {
        return $infolist
            ->schema([
                // Section 1: Status & Registrasi
                Components\\Section::make('Status Pendaftaran')
                    ->schema([
                        Components\\Grid::make(4)
                            ->schema([
                                Components\\TextEntry::make('registration_number')
                                    ->label('No. Registrasi')
                                    ->weight('bold')
                                    ->copyable(),

                                Components\\TextEntry::make('status')
                                    ->label('Status')
                                    ->badge()
                                    ->color(fn (Registration $record): string => $record->status_color)
                                    ->formatStateUsing(fn (Registration $record): string => $record->status_label),

                                Components\\TextEntry::make('payment_status')
                                    ->label('Pembayaran')
                                    ->badge()
                                    ->color(fn (string $state): string => match ($state) {
                                        'paid' => 'success',
                                        'pending' => 'warning',
                                        default => 'danger',
                                    })
                                    ->formatStateUsing(fn (Registration $record): string => $record->payment_status_label),

                                Components\\TextEntry::make('created_at')
                                    ->label('Tanggal Daftar')
                                    ->dateTime('d F Y H:i'),
                            ]),

                        Components\\Grid::make(4)
                            ->schema([
                                Components\\TextEntry::make('admissionWave.full_name')
                                    ->label('Gelombang'),

                                Components\\TextEntry::make('admissionPath.name')
                                    ->label('Jalur')
                                    ->badge()
                                    ->color('primary'),

                                Components\\TextEntry::make('firstChoice.full_name')
                                    ->label('Pilihan 1'),

                                Components\\TextEntry::make('secondChoice.full_name')
                                    ->label('Pilihan 2')
                                    ->placeholder('-'),
                            ]),
                    ]),

                // Section 2: Data Pribadi
                Components\\Section::make('Data Pribadi')
                    ->schema([
                        Components\\Grid::make(3)
                            ->schema([
                                Components\\ImageEntry::make('photo')
                                    ->label('Foto')
                                    ->circular()
                                    ->size(150)
                                    ->defaultImageUrl(asset('images/default-avatar.png')),

                                Components\\Group::make([
                                    Components\\TextEntry::make('nik')
                                        ->label('NIK'),
                                    Components\\TextEntry::make('full_name')
                                        ->label('Nama Lengkap')
                                        ->weight('bold'),
                                    Components\\TextEntry::make('nickname')
                                        ->label('Nama Panggilan')
                                        ->placeholder('-'),
                                    Components\\TextEntry::make('birth_info')
                                        ->label('Tempat, Tanggal Lahir'),
                                ]),

                                Components\\Group::make([
                                    Components\\TextEntry::make('gender_label')
                                        ->label('Jenis Kelamin'),
                                    Components\\TextEntry::make('religion_label')
                                        ->label('Agama'),
                                    Components\\TextEntry::make('blood_type')
                                        ->label('Golongan Darah')
                                        ->placeholder('-'),
                                    Components\\TextEntry::make('phone')
                                        ->label('No. HP'),
                                ]),
                            ]),

                        Components\\TextEntry::make('full_address')
                            ->label('Alamat Lengkap')
                            ->columnSpanFull(),
                    ]),

                // Section 3: Data Orang Tua
                Components\\Section::make('Data Orang Tua')
                    ->schema([
                        Components\\Grid::make(2)
                            ->schema([
                                Components\\Fieldset::make('Data Ayah')
                                    ->schema([
                                        Components\\TextEntry::make('father_name')
                                            ->label('Nama'),
                                        Components\\TextEntry::make('father_nik')
                                            ->label('NIK')
                                            ->placeholder('-'),
                                        Components\\TextEntry::make('father_education')
                                            ->label('Pendidikan')
                                            ->formatStateUsing(fn ($state) => strtoupper($state ?? '-')),
                                        Components\\TextEntry::make('father_occupation')
                                            ->label('Pekerjaan')
                                            ->placeholder('-'),
                                        Components\\TextEntry::make('father_income')
                                            ->label('Penghasilan')
                                            ->formatStateUsing(fn ($state) => match ($state) {
                                                'below_1m' => '< Rp 1.000.000',
                                                '1m_to_3m' => 'Rp 1.000.000 - 3.000.000',
                                                '3m_to_5m' => 'Rp 3.000.000 - 5.000.000',
                                                '5m_to_10m' => 'Rp 5.000.000 - 10.000.000',
                                                'above_10m' => '> Rp 10.000.000',
                                                default => '-',
                                            }),
                                        Components\\TextEntry::make('father_phone')
                                            ->label('No. HP')
                                            ->placeholder('-'),
                                    ]),

                                Components\\Fieldset::make('Data Ibu')
                                    ->schema([
                                        Components\\TextEntry::make('mother_name')
                                            ->label('Nama'),
                                        Components\\TextEntry::make('mother_nik')
                                            ->label('NIK')
                                            ->placeholder('-'),
                                        Components\\TextEntry::make('mother_education')
                                            ->label('Pendidikan')
                                            ->formatStateUsing(fn ($state) => strtoupper($state ?? '-')),
                                        Components\\TextEntry::make('mother_occupation')
                                            ->label('Pekerjaan')
                                            ->placeholder('-'),
                                        Components\\TextEntry::make('mother_income')
                                            ->label('Penghasilan')
                                            ->formatStateUsing(fn ($state) => match ($state) {
                                                'below_1m' => '< Rp 1.000.000',
                                                '1m_to_3m' => 'Rp 1.000.000 - 3.000.000',
                                                '3m_to_5m' => 'Rp 3.000.000 - 5.000.000',
                                                '5m_to_10m' => 'Rp 5.000.000 - 10.000.000',
                                                'above_10m' => '> Rp 10.000.000',
                                                default => '-',
                                            }),
                                        Components\\TextEntry::make('mother_phone')
                                            ->label('No. HP')
                                            ->placeholder('-'),
                                    ]),
                            ]),

                        Components\\Fieldset::make('Data Wali')
                            ->schema([
                                Components\\TextEntry::make('guardian_name')
                                    ->label('Nama Wali')
                                    ->placeholder('Tidak ada'),
                                Components\\TextEntry::make('guardian_relation')
                                    ->label('Hubungan')
                                    ->placeholder('-'),
                                Components\\TextEntry::make('guardian_phone')
                                    ->label('No. HP')
                                    ->placeholder('-'),
                            ])
                            ->columns(3)
                            ->hidden(fn (Registration $record): bool => empty($record->guardian_name)),
                    ])
                    ->collapsible(),

                // Section 4: Data Pendidikan
                Components\\Section::make('Data Pendidikan')
                    ->schema([
                        Components\\Grid::make(3)
                            ->schema([
                                Components\\TextEntry::make('school_name')
                                    ->label('Nama Sekolah'),
                                Components\\TextEntry::make('school_type_label')
                                    ->label('Jenis'),
                                Components\\TextEntry::make('graduation_year')
                                    ->label('Tahun Lulus'),
                            ]),

                        Components\\TextEntry::make('school_major')
                            ->label('Jurusan (SMK)')
                            ->placeholder('-')
                            ->hidden(fn (Registration $record): bool => $record->school_type !== 'smk'),

                        Components\\Grid::make(6)
                            ->schema([
                                Components\\TextEntry::make('semester_1_avg')
                                    ->label('Sem. 1')
                                    ->numeric(2),
                                Components\\TextEntry::make('semester_2_avg')
                                    ->label('Sem. 2')
                                    ->numeric(2),
                                Components\\TextEntry::make('semester_3_avg')
                                    ->label('Sem. 3')
                                    ->numeric(2),
                                Components\\TextEntry::make('semester_4_avg')
                                    ->label('Sem. 4')
                                    ->numeric(2),
                                Components\\TextEntry::make('semester_5_avg')
                                    ->label('Sem. 5')
                                    ->numeric(2),
                                Components\\TextEntry::make('average_score')
                                    ->label('Rata-rata')
                                    ->numeric(2)
                                    ->weight('bold')
                                    ->color('success'),
                            ]),
                    ])
                    ->collapsible(),

                // Section 5: Dokumen
                Components\\Section::make('Dokumen')
                    ->schema([
                        Components\\RepeatableEntry::make('document_list')
                            ->label('')
                            ->state(fn (Registration $record): array => $record->getDocumentUrls())
                            ->schema([
                                Components\\TextEntry::make('label')
                                    ->label('Dokumen'),
                                Components\\ImageEntry::make('url')
                                    ->label('Preview')
                                    ->height(100),
                            ])
                            ->columns(2)
                            ->grid(3),
                    ])
                    ->collapsible(),

                // Section 6: Ujian & Seleksi
                Components\\Section::make('Data Ujian & Seleksi')
                    ->schema([
                        Components\\Grid::make(4)
                            ->schema([
                                Components\\TextEntry::make('exam_number')
                                    ->label('No. Peserta')
                                    ->placeholder('Belum ditentukan')
                                    ->weight('bold'),
                                Components\\TextEntry::make('exam_room')
                                    ->label('Ruangan')
                                    ->placeholder('-'),
                                Components\\TextEntry::make('exam_seat')
                                    ->label('No. Kursi')
                                    ->placeholder('-'),
                            ]),

                        Components\\Grid::make(5)
                            ->schema([
                                Components\\TextEntry::make('written_exam_score')
                                    ->label('Ujian Tulis')
                                    ->numeric(2)
                                    ->placeholder('-'),
                                Components\\TextEntry::make('interview_score')
                                    ->label('Wawancara')
                                    ->numeric(2)
                                    ->placeholder('-'),
                                Components\\TextEntry::make('achievement_score')
                                    ->label('Prestasi')
                                    ->numeric(2)
                                    ->placeholder('-'),
                                Components\\TextEntry::make('final_score')
                                    ->label('Nilai Akhir')
                                    ->numeric(2)
                                    ->weight('bold')
                                    ->color('primary')
                                    ->placeholder('-'),
                                Components\\TextEntry::make('ranking')
                                    ->label('Ranking')
                                    ->placeholder('-'),
                            ]),

                        Components\\Grid::make(3)
                            ->schema([
                                Components\\TextEntry::make('result')
                                    ->label('Hasil Seleksi')
                                    ->badge()
                                    ->color(fn (Registration $record): string => $record->result_color)
                                    ->formatStateUsing(fn (Registration $record): ?string => $record->result_label)
                                    ->placeholder('Belum diumumkan'),
                                Components\\TextEntry::make('acceptedProgram.full_name')
                                    ->label('Diterima di')
                                    ->placeholder('-'),
                                Components\\TextEntry::make('announced_at')
                                    ->label('Tanggal Pengumuman')
                                    ->dateTime('d F Y')
                                    ->placeholder('-'),
                            ]),
                    ])
                    ->collapsible()
                    ->hidden(fn (Registration $record): bool =>
                        in_array($record->status, ['draft', 'submitted', 'paid', 'verification_failed'])
                    ),

                // Section 7: Verifikasi Info
                Components\\Section::make('Informasi Verifikasi')
                    ->schema([
                        Components\\TextEntry::make('verified_at')
                            ->label('Tanggal Verifikasi')
                            ->dateTime('d F Y H:i')
                            ->placeholder('Belum diverifikasi'),
                        Components\\TextEntry::make('verifiedByUser.name')
                            ->label('Diverifikasi Oleh')
                            ->placeholder('-'),
                        Components\\TextEntry::make('verification_notes')
                            ->label('Catatan Verifikasi')
                            ->placeholder('Tidak ada catatan')
                            ->columnSpanFull(),
                    ])
                    ->columns(2)
                    ->collapsible()
                    ->hidden(fn (Registration $record): bool => $record->verified_at === null),
            ]);
    }
}

Manual Review (20%):

  • ✅ Comprehensive infolist dengan semua data
  • ✅ Collapsible sections
  • ✅ Conditional visibility berdasarkan status
  • ✅ Document preview

Prompt 3: VerifyRegistration Page

Buatkan custom page VerifyRegistration untuk verifikasi dokumen:

- Tampilkan semua dokumen pendaftar dengan preview besar
- Checklist untuk setiap dokumen (valid/invalid)
- Textarea untuk catatan verifikasi
- 2 tombol: Approve (status → verified) dan Reject (status → verification_failed)
- Setelah approve, generate nomor peserta ujian

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/RegistrationResource/Pages/VerifyRegistration.php

namespace App\\Filament\\Admin\\Resources\\RegistrationResource\\Pages;

use App\\Filament\\Admin\\Resources\\RegistrationResource;
use App\\Models\\Registration;
use App\\Models\\ExamSchedule;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Forms\\Concerns\\InteractsWithForms;
use Filament\\Forms\\Contracts\\HasForms;
use Filament\\Notifications\\Notification;
use Filament\\Resources\\Pages\\Page;
use Illuminate\\Support\\Str;

class VerifyRegistration extends Page implements HasForms
{
    use InteractsWithForms;

    protected static string $resource = RegistrationResource::class;
    protected static string $view = 'filament.admin.resources.registration-resource.pages.verify-registration';

    public Registration $record;

    public ?array $data = [];

    public function mount(Registration $record): void
    {
        $this->record = $record;

        if ($record->status !== 'paid') {
            Notification::make()
                ->title('Pendaftar ini tidak dalam status menunggu verifikasi')
                ->danger()
                ->send();

            $this->redirect(RegistrationResource::getUrl('view', ['record' => $record]));
            return;
        }

        $this->form->fill([
            'verification_notes' => $record->verification_notes,
        ]);
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Checklist Dokumen')
                    ->schema([
                        Forms\\Components\\CheckboxList::make('document_checklist')
                            ->label('')
                            ->options($this->getDocumentOptions())
                            ->descriptions($this->getDocumentDescriptions())
                            ->columns(2)
                            ->bulkToggleable(),
                    ]),

                Forms\\Components\\Section::make('Catatan Verifikasi')
                    ->schema([
                        Forms\\Components\\Textarea::make('verification_notes')
                            ->label('Catatan (opsional)')
                            ->rows(3)
                            ->placeholder('Tambahkan catatan jika ada dokumen yang kurang atau perlu diperbaiki...')
                            ->columnSpanFull(),
                    ]),
            ])
            ->statePath('data');
    }

    protected function getDocumentOptions(): array
    {
        $options = [];

        $documents = [
            'photo' => 'Pas Foto',
            'ktp_scan' => 'KTP',
            'family_card_scan' => 'Kartu Keluarga',
            'birth_certificate_scan' => 'Akta Lahir',
            'diploma_scan' => 'Ijazah/SKL',
            'transcript_scan' => 'Transkrip Nilai',
            'health_certificate_scan' => 'Surat Kesehatan',
        ];

        foreach ($documents as $field => $label) {
            if ($this->record->{$field}) {
                $options[$field] = $label;
            }
        }

        // Achievement certificates
        if (!empty($this->record->achievement_certificates)) {
            foreach ($this->record->achievement_certificates as $index => $cert) {
                $options["achievement_{$index}"] = "Sertifikat Prestasi " . ($index + 1);
            }
        }

        // KIP card for KIP path
        if ($this->record->kip_card_scan) {
            $options['kip_card_scan'] = 'Kartu KIP';
        }

        return $options;
    }

    protected function getDocumentDescriptions(): array
    {
        $descriptions = [];

        // Could add preview URLs as descriptions
        foreach ($this->getDocumentOptions() as $field => $label) {
            $descriptions[$field] = 'Klik untuk preview';
        }

        return $descriptions;
    }

    public function approve(): void
    {
        $data = $this->form->getState();

        // Generate exam number
        $examNumber = $this->generateExamNumber();

        // Assign exam room and seat
        $examAssignment = $this->assignExamRoom();

        $this->record->update([
            'status' => 'verified',
            'verified_at' => now(),
            'verified_by' => auth()->id(),
            'verification_notes' => $data['verification_notes'],
            'exam_number' => $examNumber,
            'exam_room' => $examAssignment['room'],
            'exam_seat' => $examAssignment['seat'],
        ]);

        Notification::make()
            ->title('Verifikasi Berhasil')
            ->body("Nomor peserta: {$examNumber}")
            ->success()
            ->send();

        $this->redirect(RegistrationResource::getUrl('view', ['record' => $this->record]));
    }

    public function reject(): void
    {
        $data = $this->form->getState();

        if (empty($data['verification_notes'])) {
            Notification::make()
                ->title('Catatan wajib diisi')
                ->body('Jelaskan alasan penolakan dokumen.')
                ->danger()
                ->send();
            return;
        }

        $this->record->update([
            'status' => 'verification_failed',
            'verification_notes' => $data['verification_notes'],
            'verified_at' => now(),
            'verified_by' => auth()->id(),
        ]);

        Notification::make()
            ->title('Dokumen Ditolak')
            ->body('Pendaftar akan diberitahu untuk memperbaiki dokumen.')
            ->warning()
            ->send();

        $this->redirect(RegistrationResource::getUrl('view', ['record' => $this->record]));
    }

    protected function generateExamNumber(): string
    {
        $wave = $this->record->admissionWave;
        $path = $this->record->admissionPath;

        $year = $wave->year;
        $waveCode = Str::padLeft($wave->id, 2, '0');
        $pathCode = $path->code;

        // Get next sequence for this wave + path combination
        $lastNumber = Registration::where('admission_wave_id', $wave->id)
            ->where('admission_path_id', $path->id)
            ->whereNotNull('exam_number')
            ->count();

        $sequence = Str::padLeft($lastNumber + 1, 5, '0');

        return "{$year}-{$waveCode}-{$pathCode}-{$sequence}";
    }

    protected function assignExamRoom(): array
    {
        $wave = $this->record->admissionWave;

        // Get written exam schedule
        $examSchedule = ExamSchedule::where('admission_wave_id', $wave->id)
            ->where('exam_type', 'written')
            ->first();

        if (!$examSchedule || empty($examSchedule->rooms)) {
            return ['room' => null, 'seat' => null];
        }

        // Count existing assignments per room
        $rooms = $examSchedule->rooms;

        foreach ($rooms as $roomData) {
            $roomName = $roomData['room'];
            $capacity = $roomData['capacity'];

            $assigned = Registration::where('admission_wave_id', $wave->id)
                ->where('exam_room', $roomName)
                ->count();

            if ($assigned < $capacity) {
                return [
                    'room' => $roomName,
                    'seat' => $assigned + 1,
                ];
            }
        }

        // All rooms full, assign to first room (overflow)
        return [
            'room' => $rooms[0]['room'] ?? 'TBA',
            'seat' => null,
        ];
    }

    protected function getHeaderActions(): array
    {
        return [];
    }
}

Blade View:

{{-- resources/views/filament/admin/resources/registration-resource/pages/verify-registration.blade.php --}}

<x-filament-panels::page>
    <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {{-- Left: Document Preview --}}
        <div class="space-y-4">
            <h3 class="text-lg font-semibold">Preview Dokumen</h3>

            @php $documents = $this->record->getDocumentUrls(); @endphp

            @forelse($documents as $doc)
                <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
                    <h4 class="font-medium mb-2">{{ $doc['label'] }}</h4>
                    <a href="{{ $doc['url'] }}" target="_blank" class="block">
                        <img src="{{ $doc['url'] }}" alt="{{ $doc['label'] }}"
                             class="max-h-64 object-contain rounded border">
                    </a>
                </div>
            @empty
                <p class="text-gray-500">Tidak ada dokumen yang diupload.</p>
            @endforelse
        </div>

        {{-- Right: Verification Form --}}
        <div>
            <h3 class="text-lg font-semibold mb-4">Data Pendaftar</h3>

            <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
                <dl class="grid grid-cols-2 gap-4 text-sm">
                    <div>
                        <dt class="text-gray-500">Nama</dt>
                        <dd class="font-medium">{{ $this->record->full_name }}</dd>
                    </div>
                    <div>
                        <dt class="text-gray-500">NIK</dt>
                        <dd class="font-medium">{{ $this->record->nik }}</dd>
                    </div>
                    <div>
                        <dt class="text-gray-500">Jalur</dt>
                        <dd class="font-medium">{{ $this->record->admissionPath->name }}</dd>
                    </div>
                    <div>
                        <dt class="text-gray-500">Pilihan 1</dt>
                        <dd class="font-medium">{{ $this->record->firstChoice->name }}</dd>
                    </div>
                </dl>
            </div>

            <form wire:submit.prevent>
                {{ $this->form }}

                <div class="flex gap-4 mt-6">
                    <x-filament::button color="success" wire:click="approve" class="flex-1">
                        <x-heroicon-o-check-circle class="w-5 h-5 mr-2" />
                        Approve & Generate Nomor Peserta
                    </x-filament::button>

                    <x-filament::button color="danger" wire:click="reject" class="flex-1">
                        <x-heroicon-o-x-circle class="w-5 h-5 mr-2" />
                        Reject
                    </x-filament::button>
                </div>
            </form>
        </div>
    </div>
</x-filament-panels::page>

Manual Review (20%):

  • ✅ Document preview
  • ✅ Checklist validation
  • ✅ Auto-generate exam number
  • ✅ Auto-assign room & seat
  • ⚠️ Perlu handle edge case jika semua ruangan penuh

Prompt 4: InputScore Page

Buatkan custom page InputScore untuk input nilai ujian:

- Tampilkan info pendaftar (nama, jalur, prodi)
- Form input: written_exam_score, interview_score, achievement_score
- Auto-calculate final_score berdasarkan bobot jalur
- Tampilkan preview final_score
- Tombol simpan

Response Claude (80%):

<?php

// app/Filament/Admin/Resources/RegistrationResource/Pages/InputScore.php

namespace App\\Filament\\Admin\\Resources\\RegistrationResource\\Pages;

use App\\Filament\\Admin\\Resources\\RegistrationResource;
use App\\Models\\Registration;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Forms\\Concerns\\InteractsWithForms;
use Filament\\Forms\\Contracts\\HasForms;
use Filament\\Notifications\\Notification;
use Filament\\Resources\\Pages\\Page;

class InputScore extends Page implements HasForms
{
    use InteractsWithForms;

    protected static string $resource = RegistrationResource::class;
    protected static string $view = 'filament.admin.resources.registration-resource.pages.input-score';

    public Registration $record;
    public ?array $data = [];
    public ?float $previewFinalScore = null;

    public function mount(Registration $record): void
    {
        $this->record = $record;

        if (!in_array($record->status, ['verified', 'exam_scheduled', 'exam_completed', 'selection'])) {
            Notification::make()
                ->title('Tidak dapat input nilai untuk pendaftar ini')
                ->danger()
                ->send();

            $this->redirect(RegistrationResource::getUrl('view', ['record' => $record]));
            return;
        }

        $this->form->fill([
            'written_exam_score' => $record->written_exam_score,
            'interview_score' => $record->interview_score,
            'achievement_score' => $record->achievement_score,
        ]);

        $this->calculatePreview();
    }

    public function form(Form $form): Form
    {
        $weights = $this->record->admissionPath->exam_weights ?? [];

        return $form
            ->schema([
                Forms\\Components\\Section::make('Input Nilai Ujian')
                    ->description($this->getWeightsDescription())
                    ->schema([
                        Forms\\Components\\TextInput::make('written_exam_score')
                            ->label('Nilai Ujian Tulis')
                            ->numeric()
                            ->minValue(0)
                            ->maxValue(100)
                            ->step(0.01)
                            ->suffix('/ 100')
                            ->live(onBlur: true)
                            ->afterStateUpdated(fn () => $this->calculatePreview())
                            ->visible(fn () => isset($weights['written_exam'])),

                        Forms\\Components\\TextInput::make('interview_score')
                            ->label('Nilai Wawancara')
                            ->numeric()
                            ->minValue(0)
                            ->maxValue(100)
                            ->step(0.01)
                            ->suffix('/ 100')
                            ->live(onBlur: true)
                            ->afterStateUpdated(fn () => $this->calculatePreview())
                            ->visible(fn () => isset($weights['interview'])),

                        Forms\\Components\\TextInput::make('achievement_score')
                            ->label('Nilai Prestasi')
                            ->numeric()
                            ->minValue(0)
                            ->maxValue(100)
                            ->step(0.01)
                            ->suffix('/ 100')
                            ->live(onBlur: true)
                            ->afterStateUpdated(fn () => $this->calculatePreview())
                            ->visible(fn () => isset($weights['achievement'])),
                    ])
                    ->columns(3),
            ])
            ->statePath('data');
    }

    protected function getWeightsDescription(): string
    {
        $weights = $this->record->admissionPath->exam_weights ?? [];
        $parts = [];

        if (isset($weights['written_exam'])) {
            $parts[] = "Ujian Tulis: {$weights['written_exam']}%";
        }
        if (isset($weights['interview'])) {
            $parts[] = "Wawancara: {$weights['interview']}%";
        }
        if (isset($weights['achievement'])) {
            $parts[] = "Prestasi: {$weights['achievement']}%";
        }
        if (isset($weights['report_card'])) {
            $parts[] = "Nilai Rapor: {$weights['report_card']}% (otomatis dari data pendaftaran)";
        }

        return 'Bobot penilaian: ' . implode(', ', $parts);
    }

    public function calculatePreview(): void
    {
        $data = $this->form->getState();
        $weights = $this->record->admissionPath->exam_weights ?? [];

        $totalWeight = 0;
        $weightedScore = 0;

        if (isset($weights['written_exam']) && !empty($data['written_exam_score'])) {
            $weightedScore += ($data['written_exam_score'] * $weights['written_exam'] / 100);
            $totalWeight += $weights['written_exam'];
        }

        if (isset($weights['interview']) && !empty($data['interview_score'])) {
            $weightedScore += ($data['interview_score'] * $weights['interview'] / 100);
            $totalWeight += $weights['interview'];
        }

        if (isset($weights['achievement']) && !empty($data['achievement_score'])) {
            $weightedScore += ($data['achievement_score'] * $weights['achievement'] / 100);
            $totalWeight += $weights['achievement'];
        }

        if (isset($weights['report_card']) && $this->record->average_score) {
            $weightedScore += ($this->record->average_score * $weights['report_card'] / 100);
            $totalWeight += $weights['report_card'];
        }

        if ($totalWeight > 0) {
            $this->previewFinalScore = round(($weightedScore / $totalWeight) * 100, 2);
        } else {
            $this->previewFinalScore = null;
        }
    }

    public function save(): void
    {
        $data = $this->form->getState();

        $this->record->update([
            'written_exam_score' => $data['written_exam_score'] ?? null,
            'interview_score' => $data['interview_score'] ?? null,
            'achievement_score' => $data['achievement_score'] ?? null,
            'status' => 'exam_completed',
        ]);

        // Recalculate final score using model method
        $this->record->calculateFinalScore();
        $this->record->save();

        Notification::make()
            ->title('Nilai Berhasil Disimpan')
            ->body("Nilai akhir: {$this->record->final_score}")
            ->success()
            ->send();

        $this->redirect(RegistrationResource::getUrl('view', ['record' => $this->record]));
    }
}

Blade View:

{{-- resources/views/filament/admin/resources/registration-resource/pages/input-score.blade.php --}}

<x-filament-panels::page>
    {{-- Applicant Info --}}
    <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
        <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
            <div>
                <span class="text-sm text-gray-500">Nama</span>
                <p class="font-semibold">{{ $this->record->full_name }}</p>
            </div>
            <div>
                <span class="text-sm text-gray-500">No. Peserta</span>
                <p class="font-semibold">{{ $this->record->exam_number ?? '-' }}</p>
            </div>
            <div>
                <span class="text-sm text-gray-500">Jalur</span>
                <p class="font-semibold">{{ $this->record->admissionPath->name }}</p>
            </div>
            <div>
                <span class="text-sm text-gray-500">Pilihan 1</span>
                <p class="font-semibold">{{ $this->record->firstChoice->name }}</p>
            </div>
        </div>

        @if($this->record->average_score)
        <div class="mt-4 pt-4 border-t">
            <span class="text-sm text-gray-500">Nilai Rata-rata Rapor</span>
            <p class="font-semibold text-lg text-primary-600">{{ number_format($this->record->average_score, 2) }}</p>
        </div>
        @endif
    </div>

    {{-- Score Form --}}
    <form wire:submit.prevent="save">
        {{ $this->form }}

        {{-- Preview Final Score --}}
        <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 mt-6">
            <h3 class="text-lg font-semibold mb-2">Preview Nilai Akhir</h3>

            @if($this->previewFinalScore !== null)
                <p class="text-4xl font-bold text-primary-600">
                    {{ number_format($this->previewFinalScore, 2) }}
                </p>
                <p class="text-sm text-gray-500 mt-1">
                    Nilai ini dihitung berdasarkan bobot jalur {{ $this->record->admissionPath->name }}
                </p>
            @else
                <p class="text-gray-500">Masukkan nilai untuk melihat preview</p>
            @endif
        </div>

        <div class="flex justify-end mt-6">
            <x-filament::button type="submit" size="lg">
                <x-heroicon-o-check class="w-5 h-5 mr-2" />
                Simpan Nilai
            </x-filament::button>
        </div>
    </form>
</x-filament-panels::page>

Manual Review (20%):

  • ✅ Dynamic form berdasarkan exam_weights
  • ✅ Live preview final score
  • ✅ Include nilai rapor dalam kalkulasi
  • ✅ Status update ke exam_completed

Prompt 5: Bulk Actions — Generate Ranking & Announcement

Buatkan bulk action di RegistrationResource untuk:
1. Generate ranking per program studi
2. Set result (passed/failed/waitlist) berdasarkan kuota
3. Bulk update status ke 'announced'

Ranking dihitung dari final_score tertinggi ke terendah.
Passed = ranking <= kuota program studi pilihan 1
Jika tidak lolos pilihan 1, cek pilihan 2
Waitlist = cadangan (misalnya 10% dari kuota)

Response Claude (80%):

// Tambahkan di RegistrationResource bulkActions

Tables\\Actions\\BulkAction::make('generateAnnouncement')
    ->label('Generate Pengumuman')
    ->icon('heroicon-o-megaphone')
    ->color('success')
    ->requiresConfirmation()
    ->modalHeading('Generate Pengumuman Kelulusan')
    ->modalDescription('Proses ini akan menghitung ranking dan menentukan hasil seleksi berdasarkan kuota program studi. Pastikan semua nilai sudah diinput.')
    ->form([
        Forms\\Components\\Select::make('wave_id')
            ->label('Gelombang')
            ->options(fn () => AdmissionWave::where('is_active', true)->pluck('name', 'id'))
            ->required(),
    ])
    ->action(function (array $data) {
        $waveId = $data['wave_id'];

        // Get all completed registrations for this wave
        $registrations = Registration::where('admission_wave_id', $waveId)
            ->whereIn('status', ['exam_completed', 'selection'])
            ->whereNotNull('final_score')
            ->orderByDesc('final_score')
            ->get();

        // Group by first choice program
        $byFirstChoice = $registrations->groupBy('first_choice_id');

        foreach ($byFirstChoice as $programId => $regs) {
            $program = StudyProgram::find($programId);
            $quota = $program->quota;
            $waitlistQuota = ceil($quota * 0.1); // 10% cadangan

            $rank = 1;
            foreach ($regs->sortByDesc('final_score') as $reg) {
                $reg->ranking = $rank;

                if ($rank <= $quota) {
                    $reg->result = 'passed';
                    $reg->accepted_program_id = $programId;
                } elseif ($rank <= $quota + $waitlistQuota) {
                    $reg->result = 'waitlist';
                } else {
                    // Check second choice
                    $secondResult = self::checkSecondChoice($reg, $waveId);
                    $reg->result = $secondResult['result'];
                    $reg->accepted_program_id = $secondResult['program_id'];
                }

                $reg->status = 'announced';
                $reg->announced_at = now();
                $reg->save();

                $rank++;
            }
        }

        Notification::make()
            ->title('Pengumuman Berhasil Di-generate')
            ->body('Total ' . $registrations->count() . ' pendaftar telah diproses.')
            ->success()
            ->send();
    }),

Helper method (tambahkan di RegistrationResource atau Service class):

protected static function checkSecondChoice(Registration $registration, int $waveId): array
{
    if (!$registration->second_choice_id) {
        return ['result' => 'failed', 'program_id' => null];
    }

    $program = StudyProgram::find($registration->second_choice_id);
    $quota = $program->quota;

    // Count already accepted in second choice
    $acceptedCount = Registration::where('admission_wave_id', $waveId)
        ->where('accepted_program_id', $registration->second_choice_id)
        ->where('result', 'passed')
        ->count();

    if ($acceptedCount < $quota) {
        return ['result' => 'passed', 'program_id' => $registration->second_choice_id];
    }

    // Check waitlist
    $waitlistQuota = ceil($quota * 0.1);
    $waitlistCount = Registration::where('admission_wave_id', $waveId)
        ->where('accepted_program_id', $registration->second_choice_id)
        ->where('result', 'waitlist')
        ->count();

    if ($waitlistCount < $waitlistQuota) {
        return ['result' => 'waitlist', 'program_id' => $registration->second_choice_id];
    }

    return ['result' => 'failed', 'program_id' => null];
}

Manual Review (20%):

  • ✅ Ranking per program
  • ✅ Quota-based selection
  • ✅ Second choice fallback
  • ✅ 10% waitlist
  • ⚠️ Logic ini simplified — di production perlu pertimbangkan tie-breaker, passing grade, dll
💡 RECAP REGISTRATION MANAGEMENT:

Prompts: 5
Pages created:
- ListRegistrations (table with filters & bulk actions)
- ViewRegistration (comprehensive infolist)
- VerifyRegistration (document verification workflow)
- InputScore (score input with live preview)

Features:
- Navigation badge for pending verification
- Document preview with checklist
- Auto-generate exam number
- Auto-assign exam room & seat
- Live final score calculation
- Bulk ranking generation
- Quota-based selection logic
- Second choice fallback

Yang di-review manual:
- Exam number format
- Room assignment algorithm
- Score calculation formula
- Selection/ranking logic
- Tie-breaker rules

Di bagian selanjutnya, kita akan buat frontend untuk pendaftar — multi-step form wizard dengan 5 tahap untuk mengisi formulir pendaftaran lengkap.

Bagian 7: Frontend Pendaftar — Multi-Step Form Wizard

Sekarang kita buat form pendaftaran untuk calon mahasiswa. Form ini akan punya 5 tahap (wizard) yang harus diisi secara berurutan. Kita akan pakai Filament Wizard component di Applicant Panel.

Struktur 5 Tahap Pendaftaran

STEP 1: Pilih Gelombang, Jalur & Program Studi
         ↓
STEP 2: Data Pribadi (NIK, nama, TTL, alamat)
         ↓
STEP 3: Data Orang Tua/Wali
         ↓
STEP 4: Data Pendidikan & Nilai Rapor
         ↓
STEP 5: Upload Dokumen
         ↓
      SUBMIT → Lanjut ke Pembayaran

Prompt 1: RegistrationForm Page — Setup

Buatkan Filament Page untuk Applicant Panel bernama
RegistrationForm dengan Wizard. Page ini untuk mengisi
formulir pendaftaran mahasiswa baru.

Setup dasar:
- Check apakah user sudah punya registration
- Jika sudah ada, load data existing (edit mode)
- Jika belum, create new (create mode)
- Auto-save draft setiap pindah step

Response Claude (80%):

<?php

// app/Filament/Applicant/Pages/RegistrationForm.php

namespace App\\Filament\\Applicant\\Pages;

use App\\Models\\Registration;
use App\\Models\\AdmissionWave;
use App\\Models\\AdmissionPath;
use App\\Models\\StudyProgram;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Forms\\Concerns\\InteractsWithForms;
use Filament\\Forms\\Contracts\\HasForms;
use Filament\\Notifications\\Notification;
use Filament\\Pages\\Page;
use Illuminate\\Support\\Facades\\Auth;

class RegistrationForm extends Page implements HasForms
{
    use InteractsWithForms;

    protected static ?string $navigationIcon = 'heroicon-o-document-text';
    protected static ?string $navigationLabel = 'Formulir Pendaftaran';
    protected static ?string $title = 'Formulir Pendaftaran';
    protected static ?int $navigationSort = 1;

    protected static string $view = 'filament.applicant.pages.registration-form';

    public ?Registration $registration = null;
    public ?array $data = [];

    public function mount(): void
    {
        $user = Auth::user();

        // Check existing registration
        $this->registration = Registration::where('user_id', $user->id)
            ->whereNotIn('status', ['cancelled'])
            ->first();

        if ($this->registration) {
            // Check if already submitted
            if (!$this->registration->canEditForm()) {
                $this->redirect(route('filament.applicant.pages.dashboard'));
                return;
            }

            // Load existing data
            $this->form->fill($this->registration->toArray());
        } else {
            // Initialize with user data
            $this->form->fill([
                'user_id' => $user->id,
            ]);
        }
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Wizard::make([
                    $this->getStep1(),
                    $this->getStep2(),
                    $this->getStep3(),
                    $this->getStep4(),
                    $this->getStep5(),
                ])
                ->submitAction(view('filament.applicant.components.submit-button'))
                ->persistStepInQueryString()
                ->afterStateUpdated(fn () => $this->saveDraft()),
            ])
            ->statePath('data');
    }

    protected function saveDraft(): void
    {
        if ($this->registration) {
            $this->registration->update($this->form->getState());
        } else {
            $data = $this->form->getState();
            $data['user_id'] = Auth::id();
            $data['status'] = 'draft';

            $this->registration = Registration::create($data);
        }
    }

    public function submit(): void
    {
        $data = $this->form->getState();

        // Final validation
        $this->validate();

        if ($this->registration) {
            $this->registration->update(array_merge($data, [
                'status' => 'submitted',
                'submitted_at' => now(),
            ]));
        } else {
            $data['user_id'] = Auth::id();
            $data['status'] = 'submitted';
            $data['submitted_at'] = now();

            $this->registration = Registration::create($data);
        }

        Notification::make()
            ->title('Formulir Berhasil Dikirim!')
            ->body('Silakan lanjutkan ke pembayaran.')
            ->success()
            ->send();

        $this->redirect(route('filament.applicant.pages.payment'));
    }

    // Step definitions akan di-define di bawah...
}

Manual Review (20%):

  • ✅ Check existing registration
  • ✅ canEditForm() check
  • ✅ Auto-save draft
  • ✅ Persist step in query string

Prompt 2: Step 1 — Pilihan Program

Buatkan Step 1 wizard untuk pilih:
- Gelombang (hanya yang status 'open')
- Jalur pendaftaran (tampilkan biaya)
- Program studi pilihan 1
- Program studi pilihan 2 (exclude pilihan 1)

Dengan validasi dan dependent selects.

Response Claude (80%):

// Tambahkan method di RegistrationForm.php

protected function getStep1(): Forms\\Components\\Wizard\\Step
{
    return Forms\\Components\\Wizard\\Step::make('Pilihan Program')
        ->icon('heroicon-o-academic-cap')
        ->description('Pilih gelombang, jalur, dan program studi')
        ->schema([
            Forms\\Components\\Section::make()
                ->schema([
                    Forms\\Components\\Select::make('admission_wave_id')
                        ->label('Gelombang Pendaftaran')
                        ->options(function () {
                            return AdmissionWave::where('is_active', true)
                                ->where('status', 'open')
                                ->get()
                                ->mapWithKeys(fn ($wave) => [
                                    $wave->id => "{$wave->name} - {$wave->year} ({$wave->registration_period})"
                                ]);
                        })
                        ->required()
                        ->searchable()
                        ->live()
                        ->helperText('Pilih gelombang pendaftaran yang sedang dibuka'),

                    Forms\\Components\\Select::make('admission_path_id')
                        ->label('Jalur Pendaftaran')
                        ->options(function () {
                            return AdmissionPath::where('is_active', true)
                                ->get()
                                ->mapWithKeys(fn ($path) => [
                                    $path->id => "{$path->name} - {$path->formatted_fee}"
                                ]);
                        })
                        ->required()
                        ->searchable()
                        ->live()
                        ->afterStateUpdated(function ($state, Forms\\Set $set) {
                            $path = AdmissionPath::find($state);
                            if ($path) {
                                $set('payment_amount', $path->registration_fee);
                            }
                        })
                        ->helperText(function ($state) {
                            if (!$state) return null;
                            $path = AdmissionPath::find($state);
                            if (!$path) return null;

                            $requirements = $path->requirements ?? [];
                            if (empty($requirements)) return null;

                            return 'Syarat: ' . implode(', ', array_slice($requirements, 0, 2)) . '...';
                        }),

                    Forms\\Components\\Hidden::make('payment_amount'),
                ])
                ->columns(2),

            Forms\\Components\\Section::make('Pilihan Program Studi')
                ->schema([
                    Forms\\Components\\Select::make('first_choice_id')
                        ->label('Pilihan 1 (Prioritas Utama)')
                        ->options(function () {
                            return StudyProgram::where('is_active', true)
                                ->with('faculty')
                                ->get()
                                ->mapWithKeys(fn ($program) => [
                                    $program->id => "[{$program->faculty->code}] {$program->degree} {$program->name} - {$program->accreditation}"
                                ]);
                        })
                        ->required()
                        ->searchable()
                        ->live()
                        ->helperText('Program studi utama yang Anda inginkan'),

                    Forms\\Components\\Select::make('second_choice_id')
                        ->label('Pilihan 2 (Alternatif)')
                        ->options(function (Forms\\Get $get) {
                            $firstChoice = $get('first_choice_id');

                            return StudyProgram::where('is_active', true)
                                ->when($firstChoice, fn ($q) => $q->where('id', '!=', $firstChoice))
                                ->with('faculty')
                                ->get()
                                ->mapWithKeys(fn ($program) => [
                                    $program->id => "[{$program->faculty->code}] {$program->degree} {$program->name}"
                                ]);
                        })
                        ->searchable()
                        ->helperText('Opsional. Pilihan cadangan jika tidak diterima di pilihan 1'),
                ])
                ->columns(2),

            Forms\\Components\\Placeholder::make('info')
                ->content(function (Forms\\Get $get) {
                    $firstId = $get('first_choice_id');
                    if (!$firstId) return '';

                    $program = StudyProgram::with('faculty')->find($firstId);
                    if (!$program) return '';

                    return new \\Illuminate\\Support\\HtmlString("
                        <div class='p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg'>
                            <h4 class='font-semibold text-blue-800 dark:text-blue-200'>Info Program Studi Pilihan 1</h4>
                            <p class='text-sm mt-2'>
                                <strong>Fakultas:</strong> {$program->faculty->name}<br>
                                <strong>Akreditasi:</strong> {$program->accreditation}<br>
                                <strong>Kuota:</strong> {$program->quota} mahasiswa<br>
                                <strong>UKT:</strong> {$program->formatted_tuition}/semester
                            </p>
                        </div>
                    ");
                })
                ->columnSpanFull(),
        ])
        ->afterValidation(fn () => $this->saveDraft());
}

Manual Review (20%):

  • ✅ Only open waves
  • ✅ Fee display di jalur
  • ✅ Exclude first choice dari second choice
  • ✅ Info box untuk program detail

Prompt 3: Step 2 — Data Pribadi

Buatkan Step 2 untuk data pribadi:
- NIK dengan validasi 16 digit dan format Indonesia
- Nama lengkap, nama panggilan
- Tempat & tanggal lahir
- Jenis kelamin (radio)
- Agama, golongan darah
- No HP
- Alamat lengkap dengan fields terpisah

Response Claude (80%):

protected function getStep2(): Forms\\Components\\Wizard\\Step
{
    return Forms\\Components\\Wizard\\Step::make('Data Pribadi')
        ->icon('heroicon-o-user')
        ->description('Isi data diri Anda dengan lengkap')
        ->schema([
            Forms\\Components\\Section::make('Identitas')
                ->schema([
                    Forms\\Components\\TextInput::make('nik')
                        ->label('NIK (Nomor Induk Kependudukan)')
                        ->required()
                        ->length(16)
                        ->numeric()
                        ->unique(Registration::class, 'nik', ignoreRecord: true)
                        ->validationMessages([
                            'length' => 'NIK harus 16 digit',
                            'unique' => 'NIK sudah terdaftar',
                        ])
                        ->helperText('Sesuai KTP/KK, 16 digit angka')
                        ->placeholder('3201234567890001'),

                    Forms\\Components\\TextInput::make('full_name')
                        ->label('Nama Lengkap')
                        ->required()
                        ->maxLength(255)
                        ->placeholder('Sesuai ijazah/akta lahir'),

                    Forms\\Components\\TextInput::make('nickname')
                        ->label('Nama Panggilan')
                        ->maxLength(50)
                        ->placeholder('Nama yang biasa dipanggil'),
                ])
                ->columns(3),

            Forms\\Components\\Section::make('Kelahiran & Identitas Lainnya')
                ->schema([
                    Forms\\Components\\TextInput::make('birth_place')
                        ->label('Tempat Lahir')
                        ->required()
                        ->maxLength(100)
                        ->placeholder('Kota/Kabupaten'),

                    Forms\\Components\\DatePicker::make('birth_date')
                        ->label('Tanggal Lahir')
                        ->required()
                        ->native(false)
                        ->displayFormat('d F Y')
                        ->maxDate(now()->subYears(15))
                        ->minDate(now()->subYears(50)),

                    Forms\\Components\\Radio::make('gender')
                        ->label('Jenis Kelamin')
                        ->options([
                            'male' => 'Laki-laki',
                            'female' => 'Perempuan',
                        ])
                        ->required()
                        ->inline(),

                    Forms\\Components\\Select::make('religion')
                        ->label('Agama')
                        ->options([
                            'islam' => 'Islam',
                            'kristen' => 'Kristen',
                            'katolik' => 'Katolik',
                            'hindu' => 'Hindu',
                            'buddha' => 'Buddha',
                            'konghucu' => 'Konghucu',
                        ])
                        ->required()
                        ->searchable(),

                    Forms\\Components\\Select::make('blood_type')
                        ->label('Golongan Darah')
                        ->options([
                            'A' => 'A',
                            'B' => 'B',
                            'AB' => 'AB',
                            'O' => 'O',
                        ])
                        ->placeholder('Pilih jika diketahui'),

                    Forms\\Components\\TextInput::make('phone')
                        ->label('Nomor HP (WhatsApp)')
                        ->required()
                        ->tel()
                        ->prefix('+62')
                        ->placeholder('81234567890')
                        ->helperText('Nomor aktif untuk konfirmasi'),
                ])
                ->columns(3),

            Forms\\Components\\Section::make('Alamat Tempat Tinggal')
                ->schema([
                    Forms\\Components\\Textarea::make('address')
                        ->label('Alamat (Jalan, Nomor Rumah)')
                        ->required()
                        ->rows(2)
                        ->placeholder('Jl. Merdeka No. 123')
                        ->columnSpanFull(),

                    Forms\\Components\\Grid::make(4)
                        ->schema([
                            Forms\\Components\\TextInput::make('rt')
                                ->label('RT')
                                ->maxLength(5)
                                ->placeholder('001'),

                            Forms\\Components\\TextInput::make('rw')
                                ->label('RW')
                                ->maxLength(5)
                                ->placeholder('002'),

                            Forms\\Components\\TextInput::make('postal_code')
                                ->label('Kode Pos')
                                ->maxLength(10)
                                ->placeholder('12345'),
                        ]),

                    Forms\\Components\\Grid::make(2)
                        ->schema([
                            Forms\\Components\\TextInput::make('village')
                                ->label('Kelurahan/Desa')
                                ->required()
                                ->maxLength(100),

                            Forms\\Components\\TextInput::make('district')
                                ->label('Kecamatan')
                                ->required()
                                ->maxLength(100),

                            Forms\\Components\\TextInput::make('city')
                                ->label('Kabupaten/Kota')
                                ->required()
                                ->maxLength(100),

                            Forms\\Components\\TextInput::make('province')
                                ->label('Provinsi')
                                ->required()
                                ->maxLength(100),
                        ]),
                ]),
        ])
        ->afterValidation(fn () => $this->saveDraft());
}

Manual Review (20%):

  • ✅ NIK 16 digit validation
  • ✅ Unique NIK check
  • ✅ Age validation (15-50 tahun)
  • ✅ Alamat fields lengkap

Prompt 4: Step 3 — Data Orang Tua

Buatkan Step 3 untuk data orang tua:
- Data Ayah: nama, NIK, pendidikan, pekerjaan, penghasilan, no HP
- Data Ibu: sama seperti ayah
- Data Wali (opsional dengan toggle)
- Penghasilan dalam range (dropdown)

Response Claude (80%):

protected function getStep3(): Forms\\Components\\Wizard\\Step
{
    return Forms\\Components\\Wizard\\Step::make('Data Orang Tua')
        ->icon('heroicon-o-users')
        ->description('Isi data orang tua/wali Anda')
        ->schema([
            Forms\\Components\\Section::make('Data Ayah')
                ->schema([
                    Forms\\Components\\TextInput::make('father_name')
                        ->label('Nama Lengkap Ayah')
                        ->required()
                        ->maxLength(255),

                    Forms\\Components\\TextInput::make('father_nik')
                        ->label('NIK Ayah')
                        ->length(16)
                        ->numeric()
                        ->placeholder('Opsional'),

                    Forms\\Components\\Select::make('father_education')
                        ->label('Pendidikan Terakhir')
                        ->options([
                            'sd' => 'SD/Sederajat',
                            'smp' => 'SMP/Sederajat',
                            'sma' => 'SMA/Sederajat',
                            'd1' => 'D1',
                            'd2' => 'D2',
                            'd3' => 'D3',
                            'd4' => 'D4',
                            's1' => 'S1',
                            's2' => 'S2',
                            's3' => 'S3',
                        ]),

                    Forms\\Components\\TextInput::make('father_occupation')
                        ->label('Pekerjaan')
                        ->maxLength(100)
                        ->placeholder('Contoh: PNS, Wiraswasta, Petani'),

                    Forms\\Components\\Select::make('father_income')
                        ->label('Penghasilan per Bulan')
                        ->options([
                            'below_1m' => 'Kurang dari Rp 1.000.000',
                            '1m_to_3m' => 'Rp 1.000.000 - Rp 3.000.000',
                            '3m_to_5m' => 'Rp 3.000.000 - Rp 5.000.000',
                            '5m_to_10m' => 'Rp 5.000.000 - Rp 10.000.000',
                            'above_10m' => 'Lebih dari Rp 10.000.000',
                        ]),

                    Forms\\Components\\TextInput::make('father_phone')
                        ->label('Nomor HP')
                        ->tel()
                        ->placeholder('081234567890'),
                ])
                ->columns(3),

            Forms\\Components\\Section::make('Data Ibu')
                ->schema([
                    Forms\\Components\\TextInput::make('mother_name')
                        ->label('Nama Lengkap Ibu')
                        ->required()
                        ->maxLength(255),

                    Forms\\Components\\TextInput::make('mother_nik')
                        ->label('NIK Ibu')
                        ->length(16)
                        ->numeric()
                        ->placeholder('Opsional'),

                    Forms\\Components\\Select::make('mother_education')
                        ->label('Pendidikan Terakhir')
                        ->options([
                            'sd' => 'SD/Sederajat',
                            'smp' => 'SMP/Sederajat',
                            'sma' => 'SMA/Sederajat',
                            'd1' => 'D1',
                            'd2' => 'D2',
                            'd3' => 'D3',
                            'd4' => 'D4',
                            's1' => 'S1',
                            's2' => 'S2',
                            's3' => 'S3',
                        ]),

                    Forms\\Components\\TextInput::make('mother_occupation')
                        ->label('Pekerjaan')
                        ->maxLength(100)
                        ->placeholder('Contoh: Ibu Rumah Tangga, Guru'),

                    Forms\\Components\\Select::make('mother_income')
                        ->label('Penghasilan per Bulan')
                        ->options([
                            'below_1m' => 'Kurang dari Rp 1.000.000',
                            '1m_to_3m' => 'Rp 1.000.000 - Rp 3.000.000',
                            '3m_to_5m' => 'Rp 3.000.000 - Rp 5.000.000',
                            '5m_to_10m' => 'Rp 5.000.000 - Rp 10.000.000',
                            'above_10m' => 'Lebih dari Rp 10.000.000',
                        ]),

                    Forms\\Components\\TextInput::make('mother_phone')
                        ->label('Nomor HP')
                        ->tel()
                        ->placeholder('081234567890'),
                ])
                ->columns(3),

            Forms\\Components\\Section::make('Data Wali')
                ->description('Isi jika berbeda dengan orang tua kandung')
                ->schema([
                    Forms\\Components\\Toggle::make('has_guardian')
                        ->label('Saya memiliki wali selain orang tua kandung')
                        ->live()
                        ->dehydrated(false),

                    Forms\\Components\\Grid::make(3)
                        ->schema([
                            Forms\\Components\\TextInput::make('guardian_name')
                                ->label('Nama Wali')
                                ->maxLength(255),

                            Forms\\Components\\TextInput::make('guardian_phone')
                                ->label('Nomor HP Wali')
                                ->tel(),

                            Forms\\Components\\TextInput::make('guardian_relation')
                                ->label('Hubungan dengan Pendaftar')
                                ->placeholder('Contoh: Paman, Kakak, Kakek')
                                ->maxLength(50),
                        ])
                        ->visible(fn (Forms\\Get $get) => $get('has_guardian')),
                ])
                ->collapsible()
                ->collapsed(),
        ])
        ->afterValidation(fn () => $this->saveDraft());
}

Manual Review (20%):

  • ✅ Penghasilan dalam range
  • ✅ Pendidikan dropdown
  • ✅ Wali dengan toggle
  • ✅ Collapsible wali section

Prompt 5: Step 4 — Data Pendidikan

Buatkan Step 4 untuk data pendidikan:
- Nama sekolah, NPSN
- Jenis sekolah (SMA/SMK/MA/Paket C)
- Jurusan (jika SMK)
- Alamat sekolah
- Tahun lulus
- Nilai rata-rata rapor semester 1-5

Response Claude (80%):

protected function getStep4(): Forms\\Components\\Wizard\\Step
{
    return Forms\\Components\\Wizard\\Step::make('Data Pendidikan')
        ->icon('heroicon-o-building-library')
        ->description('Isi data sekolah dan nilai rapor')
        ->schema([
            Forms\\Components\\Section::make('Informasi Sekolah')
                ->schema([
                    Forms\\Components\\TextInput::make('school_name')
                        ->label('Nama Sekolah')
                        ->required()
                        ->maxLength(255)
                        ->placeholder('SMA Negeri 1 Jakarta'),

                    Forms\\Components\\TextInput::make('school_npsn')
                        ->label('NPSN Sekolah')
                        ->maxLength(20)
                        ->placeholder('8 digit, contoh: 20100001')
                        ->helperText('Nomor Pokok Sekolah Nasional'),

                    Forms\\Components\\Select::make('school_type')
                        ->label('Jenis Sekolah')
                        ->options([
                            'sma' => 'SMA (Sekolah Menengah Atas)',
                            'smk' => 'SMK (Sekolah Menengah Kejuruan)',
                            'ma' => 'MA (Madrasah Aliyah)',
                            'paket_c' => 'Paket C',
                        ])
                        ->required()
                        ->live(),

                    Forms\\Components\\TextInput::make('school_major')
                        ->label('Jurusan/Kompetensi Keahlian')
                        ->maxLength(100)
                        ->placeholder('Contoh: Teknik Komputer Jaringan')
                        ->visible(fn (Forms\\Get $get) => $get('school_type') === 'smk'),

                    Forms\\Components\\TextInput::make('graduation_year')
                        ->label('Tahun Lulus')
                        ->required()
                        ->numeric()
                        ->minValue(2015)
                        ->maxValue((int) date('Y') + 1)
                        ->default((int) date('Y'))
                        ->placeholder(date('Y')),
                ])
                ->columns(3),

            Forms\\Components\\Section::make('Alamat Sekolah')
                ->schema([
                    Forms\\Components\\Textarea::make('school_address')
                        ->label('Alamat Sekolah')
                        ->rows(2)
                        ->placeholder('Jalan, nomor, kelurahan')
                        ->columnSpanFull(),

                    Forms\\Components\\Grid::make(2)
                        ->schema([
                            Forms\\Components\\TextInput::make('school_city')
                                ->label('Kota/Kabupaten')
                                ->maxLength(100),

                            Forms\\Components\\TextInput::make('school_province')
                                ->label('Provinsi')
                                ->maxLength(100),
                        ]),
                ])
                ->collapsible(),

            Forms\\Components\\Section::make('Nilai Rapor')
                ->description('Masukkan rata-rata nilai rapor per semester (skala 0-100)')
                ->schema([
                    Forms\\Components\\Grid::make(5)
                        ->schema([
                            Forms\\Components\\TextInput::make('semester_1_avg')
                                ->label('Semester 1')
                                ->numeric()
                                ->minValue(0)
                                ->maxValue(100)
                                ->step(0.01)
                                ->suffix('/100')
                                ->required()
                                ->live(onBlur: true),

                            Forms\\Components\\TextInput::make('semester_2_avg')
                                ->label('Semester 2')
                                ->numeric()
                                ->minValue(0)
                                ->maxValue(100)
                                ->step(0.01)
                                ->suffix('/100')
                                ->required()
                                ->live(onBlur: true),

                            Forms\\Components\\TextInput::make('semester_3_avg')
                                ->label('Semester 3')
                                ->numeric()
                                ->minValue(0)
                                ->maxValue(100)
                                ->step(0.01)
                                ->suffix('/100')
                                ->required()
                                ->live(onBlur: true),

                            Forms\\Components\\TextInput::make('semester_4_avg')
                                ->label('Semester 4')
                                ->numeric()
                                ->minValue(0)
                                ->maxValue(100)
                                ->step(0.01)
                                ->suffix('/100')
                                ->required()
                                ->live(onBlur: true),

                            Forms\\Components\\TextInput::make('semester_5_avg')
                                ->label('Semester 5')
                                ->numeric()
                                ->minValue(0)
                                ->maxValue(100)
                                ->step(0.01)
                                ->suffix('/100')
                                ->required()
                                ->live(onBlur: true),
                        ]),

                    Forms\\Components\\Placeholder::make('average_preview')
                        ->label('Rata-rata Keseluruhan')
                        ->content(function (Forms\\Get $get) {
                            $scores = array_filter([
                                $get('semester_1_avg'),
                                $get('semester_2_avg'),
                                $get('semester_3_avg'),
                                $get('semester_4_avg'),
                                $get('semester_5_avg'),
                            ], fn ($v) => $v !== null && $v !== '');

                            if (count($scores) === 0) {
                                return 'Masukkan nilai untuk melihat rata-rata';
                            }

                            $avg = round(array_sum($scores) / count($scores), 2);

                            $color = $avg >= 80 ? 'text-green-600' : ($avg >= 70 ? 'text-blue-600' : 'text-orange-600');

                            return new \\Illuminate\\Support\\HtmlString(
                                "<span class='text-2xl font-bold {$color}'>{$avg}</span> / 100"
                            );
                        }),
                ]),
        ])
        ->afterValidation(fn () => $this->saveDraft());
}

Manual Review (20%):

  • ✅ Jurusan hanya muncul untuk SMK
  • ✅ Year validation
  • ✅ Live preview rata-rata nilai

Prompt 6: Step 5 — Upload Dokumen

Buatkan Step 5 untuk upload dokumen:
- Pas foto (3x4, max 500KB)
- Scan KTP
- Scan Kartu Keluarga
- Scan Akta Lahir
- Scan Ijazah/SKL
- Scan Transkrip Nilai
- Surat Keterangan Sehat
- Sertifikat prestasi (multiple, untuk jalur prestasi)
- Kartu KIP (untuk jalur KIP)

Dengan validasi file type dan size.

Response Claude (80%):

protected function getStep5(): Forms\\Components\\Wizard\\Step
{
    return Forms\\Components\\Wizard\\Step::make('Upload Dokumen')
        ->icon('heroicon-o-document-arrow-up')
        ->description('Upload dokumen persyaratan')
        ->schema([
            Forms\\Components\\Section::make('Dokumen Wajib')
                ->description('Format: JPG, PNG, atau PDF. Maksimal 2MB per file.')
                ->schema([
                    Forms\\Components\\FileUpload::make('photo')
                        ->label('Pas Foto (3x4)')
                        ->image()
                        ->imageEditor()
                        ->imageCropAspectRatio('3:4')
                        ->maxSize(512) // 500KB
                        ->directory('registrations/photos')
                        ->required()
                        ->helperText('Foto formal dengan latar belakang merah/biru. Max 500KB.'),

                    Forms\\Components\\FileUpload::make('ktp_scan')
                        ->label('Scan KTP')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/ktp')
                        ->required()
                        ->helperText('KTP pendaftar yang masih berlaku'),

                    Forms\\Components\\FileUpload::make('family_card_scan')
                        ->label('Scan Kartu Keluarga')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/kk')
                        ->required(),

                    Forms\\Components\\FileUpload::make('birth_certificate_scan')
                        ->label('Scan Akta Kelahiran')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/akta')
                        ->required(),

                    Forms\\Components\\FileUpload::make('diploma_scan')
                        ->label('Scan Ijazah / Surat Keterangan Lulus')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/ijazah')
                        ->required()
                        ->helperText('Ijazah SMA/SMK/MA atau SKL jika belum ada ijazah'),

                    Forms\\Components\\FileUpload::make('transcript_scan')
                        ->label('Scan Transkrip Nilai / Rapor')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/transkrip')
                        ->required()
                        ->helperText('Transkrip nilai atau rapor semester 1-5'),
                ])
                ->columns(2),

            Forms\\Components\\Section::make('Dokumen Tambahan')
                ->schema([
                    Forms\\Components\\FileUpload::make('health_certificate_scan')
                        ->label('Surat Keterangan Sehat')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/kesehatan')
                        ->helperText('Dari dokter/puskesmas/rumah sakit'),

                    Forms\\Components\\FileUpload::make('achievement_certificates')
                        ->label('Sertifikat Prestasi')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/prestasi')
                        ->multiple()
                        ->maxFiles(5)
                        ->reorderable()
                        ->helperText('Opsional. Sertifikat lomba, olimpiade, dll. Max 5 file.')
                        ->visible(fn (Forms\\Get $get) =>
                            $this->isPrestasiPath($get('admission_path_id'))
                        ),

                    Forms\\Components\\FileUpload::make('kip_card_scan')
                        ->label('Kartu KIP / Surat Keterangan Tidak Mampu')
                        ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                        ->maxSize(2048)
                        ->directory('registrations/kip')
                        ->required(fn (Forms\\Get $get) =>
                            $this->isKipPath($get('admission_path_id'))
                        )
                        ->visible(fn (Forms\\Get $get) =>
                            $this->isKipPath($get('admission_path_id'))
                        )
                        ->helperText('Kartu KIP atau SKTM dari kelurahan'),
                ])
                ->columns(2),

            Forms\\Components\\Section::make('Konfirmasi')
                ->schema([
                    Forms\\Components\\Checkbox::make('agreement')
                        ->label('Saya menyatakan bahwa seluruh data yang saya isikan adalah benar dan dapat dipertanggungjawabkan. Saya bersedia menerima sanksi pembatalan jika ditemukan pemalsuan data.')
                        ->required()
                        ->accepted()
                        ->validationMessages([
                            'accepted' => 'Anda harus menyetujui pernyataan ini',
                        ]),
                ]),
        ]);
}

// Helper methods
protected function isPrestasiPath(?int $pathId): bool
{
    if (!$pathId) return false;
    $path = AdmissionPath::find($pathId);
    return $path && str_contains(strtolower($path->code), 'prestasi');
}

protected function isKipPath(?int $pathId): bool
{
    if (!$pathId) return false;
    $path = AdmissionPath::find($pathId);
    return $path && str_contains(strtolower($path->code), 'kip');
}

Manual Review (20%):

  • ✅ Image editor untuk foto
  • ✅ Conditional fields (prestasi, KIP)
  • ✅ Multiple file upload untuk sertifikat
  • ✅ Agreement checkbox

Blade View untuk Registration Form

{{-- resources/views/filament/applicant/pages/registration-form.blade.php --}}

<x-filament-panels::page>
    @if($this->registration && $this->registration->status !== 'draft')
        <div class="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
            <div class="flex items-center gap-3">
                <x-heroicon-o-exclamation-triangle class="w-6 h-6 text-yellow-600" />
                <div>
                    <h3 class="font-semibold text-yellow-800 dark:text-yellow-200">
                        Formulir sudah dikirim
                    </h3>
                    <p class="text-sm text-yellow-700 dark:text-yellow-300">
                        Status: {{ $this->registration->status_label }}.
                        Anda tidak dapat mengedit formulir yang sudah dikirim.
                    </p>
                </div>
            </div>
        </div>
    @endif

    <form wire:submit="submit">
        {{ $this->form }}
    </form>
</x-filament-panels::page>

Submit Button Component

{{-- resources/views/filament/applicant/components/submit-button.blade.php --}}

<div class="flex items-center justify-between pt-6 border-t">
    <div class="text-sm text-gray-500">
        <x-heroicon-o-information-circle class="w-4 h-4 inline mr-1" />
        Data tersimpan otomatis setiap pindah tahap
    </div>

    <x-filament::button type="submit" size="lg" color="success">
        <x-heroicon-o-paper-airplane class="w-5 h-5 mr-2" />
        Kirim Formulir & Lanjut Pembayaran
    </x-filament::button>
</div>

Applicant Dashboard

<?php

// app/Filament/Applicant/Pages/Dashboard.php

namespace App\\Filament\\Applicant\\Pages;

use App\\Models\\Registration;
use Filament\\Pages\\Page;
use Illuminate\\Support\\Facades\\Auth;

class Dashboard extends Page
{
    protected static ?string $navigationIcon = 'heroicon-o-home';
    protected static ?string $navigationLabel = 'Dashboard';
    protected static ?string $title = 'Dashboard Pendaftaran';
    protected static ?int $navigationSort = 0;

    protected static string $view = 'filament.applicant.pages.dashboard';

    public ?Registration $registration = null;

    public function mount(): void
    {
        $this->registration = Registration::where('user_id', Auth::id())
            ->whereNotIn('status', ['cancelled'])
            ->first();
    }

    public function getStatusSteps(): array
    {
        return [
            [
                'key' => 'form',
                'label' => 'Isi Formulir',
                'completed' => $this->registration && !in_array($this->registration->status, ['draft']),
            ],
            [
                'key' => 'payment',
                'label' => 'Pembayaran',
                'completed' => $this->registration && $this->registration->payment_status === 'paid',
            ],
            [
                'key' => 'verification',
                'label' => 'Verifikasi',
                'completed' => $this->registration && in_array($this->registration->status, ['verified', 'exam_scheduled', 'exam_completed', 'selection', 'announced', 'accepted', 're_registered']),
            ],
            [
                'key' => 'exam',
                'label' => 'Ujian',
                'completed' => $this->registration && in_array($this->registration->status, ['exam_completed', 'selection', 'announced', 'accepted', 're_registered']),
            ],
            [
                'key' => 'result',
                'label' => 'Pengumuman',
                'completed' => $this->registration && in_array($this->registration->status, ['announced', 'accepted', 'rejected', 're_registered']),
            ],
        ];
    }
}

Dashboard Blade View

{{-- resources/views/filament/applicant/pages/dashboard.blade.php --}}

<x-filament-panels::page>
    {{-- Progress Steps --}}
    <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
        <h2 class="text-lg font-semibold mb-4">Progress Pendaftaran</h2>

        <div class="flex items-center justify-between">
            @foreach($this->getStatusSteps() as $index => $step)
                <div class="flex items-center {{ $index < count($this->getStatusSteps()) - 1 ? 'flex-1' : '' }}">
                    <div class="flex flex-col items-center">
                        <div class="w-10 h-10 rounded-full flex items-center justify-center {{ $step['completed'] ? 'bg-green-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-500' }}">
                            @if($step['completed'])
                                <x-heroicon-o-check class="w-5 h-5" />
                            @else
                                {{ $index + 1 }}
                            @endif
                        </div>
                        <span class="text-xs mt-2 text-center">{{ $step['label'] }}</span>
                    </div>

                    @if($index < count($this->getStatusSteps()) - 1)
                        <div class="flex-1 h-1 mx-2 {{ $step['completed'] ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700' }}"></div>
                    @endif
                </div>
            @endforeach
        </div>
    </div>

    {{-- Status Card --}}
    @if($this->registration)
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
            <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
                <h3 class="text-lg font-semibold mb-4">Status Pendaftaran</h3>

                <dl class="space-y-3">
                    <div class="flex justify-between">
                        <dt class="text-gray-500">No. Registrasi</dt>
                        <dd class="font-medium">{{ $this->registration->registration_number ?? '-' }}</dd>
                    </div>
                    <div class="flex justify-between">
                        <dt class="text-gray-500">Status</dt>
                        <dd>
                            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{{ $this->registration->status_color }}-100 text-{{ $this->registration->status_color }}-800">
                                {{ $this->registration->status_label }}
                            </span>
                        </dd>
                    </div>
                    <div class="flex justify-between">
                        <dt class="text-gray-500">Pembayaran</dt>
                        <dd>
                            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $this->registration->payment_status === 'paid' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }}">
                                {{ $this->registration->payment_status_label }}
                            </span>
                        </dd>
                    </div>
                </dl>
            </div>

            <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
                <h3 class="text-lg font-semibold mb-4">Pilihan Program</h3>

                <dl class="space-y-3">
                    <div class="flex justify-between">
                        <dt class="text-gray-500">Gelombang</dt>
                        <dd class="font-medium">{{ $this->registration->admissionWave?->full_name ?? '-' }}</dd>
                    </div>
                    <div class="flex justify-between">
                        <dt class="text-gray-500">Jalur</dt>
                        <dd class="font-medium">{{ $this->registration->admissionPath?->name ?? '-' }}</dd>
                    </div>
                    <div class="flex justify-between">
                        <dt class="text-gray-500">Pilihan 1</dt>
                        <dd class="font-medium">{{ $this->registration->firstChoice?->name ?? '-' }}</dd>
                    </div>
                </dl>
            </div>
        </div>

        {{-- Action Cards --}}
        <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
            {{-- Continue Form --}}
            @if($this->registration->canEditForm())
                <a href="{{ route('filament.applicant.pages.registration-form') }}"
                   class="block bg-blue-50 dark:bg-blue-900/20 rounded-lg p-6 hover:bg-blue-100 dark:hover:bg-blue-900/40 transition">
                    <x-heroicon-o-pencil-square class="w-8 h-8 text-blue-600 mb-3" />
                    <h4 class="font-semibold text-blue-800 dark:text-blue-200">Lanjutkan Formulir</h4>
                    <p class="text-sm text-blue-600 dark:text-blue-300 mt-1">Lengkapi data pendaftaran Anda</p>
                </a>
            @endif

            {{-- Payment --}}
            @if($this->registration->canPay())
                <a href="{{ route('filament.applicant.pages.payment') }}"
                   class="block bg-green-50 dark:bg-green-900/20 rounded-lg p-6 hover:bg-green-100 dark:hover:bg-green-900/40 transition">
                    <x-heroicon-o-banknotes class="w-8 h-8 text-green-600 mb-3" />
                    <h4 class="font-semibold text-green-800 dark:text-green-200">Bayar Pendaftaran</h4>
                    <p class="text-sm text-green-600 dark:text-green-300 mt-1">{{ $this->registration->formatted_payment_amount }}</p>
                </a>
            @endif

            {{-- Print Exam Card --}}
            @if($this->registration->canPrintExamCard())
                <a href="{{ route('applicant.exam-card.print', $this->registration) }}"
                   target="_blank"
                   class="block bg-purple-50 dark:bg-purple-900/20 rounded-lg p-6 hover:bg-purple-100 dark:hover:bg-purple-900/40 transition">
                    <x-heroicon-o-printer class="w-8 h-8 text-purple-600 mb-3" />
                    <h4 class="font-semibold text-purple-800 dark:text-purple-200">Cetak Kartu Ujian</h4>
                    <p class="text-sm text-purple-600 dark:text-purple-300 mt-1">No. Peserta: {{ $this->registration->exam_number }}</p>
                </a>
            @endif

            {{-- Re-registration --}}
            @if($this->registration->canReRegister())
                <a href="{{ route('filament.applicant.pages.re-registration') }}"
                   class="block bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-6 hover:bg-yellow-100 dark:hover:bg-yellow-900/40 transition">
                    <x-heroicon-o-academic-cap class="w-8 h-8 text-yellow-600 mb-3" />
                    <h4 class="font-semibold text-yellow-800 dark:text-yellow-200">Daftar Ulang</h4>
                    <p class="text-sm text-yellow-600 dark:text-yellow-300 mt-1">Selamat! Anda diterima</p>
                </a>
            @endif
        </div>

    @else
        {{-- No Registration Yet --}}
        <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
            <x-heroicon-o-document-plus class="w-16 h-16 text-gray-400 mx-auto mb-4" />
            <h3 class="text-xl font-semibold mb-2">Belum Ada Pendaftaran</h3>
            <p class="text-gray-500 mb-6">Mulai pendaftaran Anda sekarang untuk menjadi mahasiswa baru.</p>

            <a href="{{ route('filament.applicant.pages.registration-form') }}"
               class="inline-flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
                <x-heroicon-o-plus class="w-5 h-5 mr-2" />
                Mulai Pendaftaran
            </a>
        </div>
    @endif
</x-filament-panels::page>
💡 RECAP MULTI-STEP FORM WIZARD:

Prompts: 6
Steps created: 5
- Step 1: Pilihan Program (wave, path, prodi)
- Step 2: Data Pribadi (NIK, nama, alamat)
- Step 3: Data Orang Tua (ayah, ibu, wali)
- Step 4: Data Pendidikan (sekolah, nilai rapor)
- Step 5: Upload Dokumen (foto, KTP, dll)

Pages created:
- RegistrationForm (wizard)
- Dashboard (progress & actions)

Features:
- Auto-save draft setiap step
- Persist step di query string
- NIK 16 digit validation
- Live preview rata-rata nilai
- Conditional fields (SMK jurusan, KIP card)
- Multiple file upload (sertifikat)
- Image editor untuk foto
- Progress steps visual
- Action cards berdasarkan status

Yang di-review manual:
- NIK validation format
- File upload paths
- Agreement checkbox
- Status flow checks

Di bagian selanjutnya, kita akan buat integrasi pembayaran dengan Midtrans dan generate kartu ujian PDF.

Bagian 8: Payment Integration & Kartu Ujian PDF

Di bagian ini kita akan integrasikan pembayaran biaya pendaftaran dengan Midtrans dan buat generator kartu ujian PDF menggunakan DomPDF.

Prompt 1: Install Midtrans Package

Berikan langkah-langkah untuk:
1. Install Midtrans PHP SDK
2. Konfigurasi .env untuk Midtrans sandbox
3. Buat config file untuk Midtrans

Response Claude (80%):

# Install Midtrans SDK
composer require midtrans/midtrans-php
# .env - Midtrans Sandbox Credentials
MIDTRANS_SERVER_KEY=SB-Mid-server-xxxxxxxxxxxxx
MIDTRANS_CLIENT_KEY=SB-Mid-client-xxxxxxxxxxxxx
MIDTRANS_IS_PRODUCTION=false
MIDTRANS_IS_SANITIZED=true
MIDTRANS_IS_3DS=true
<?php

// config/midtrans.php

return [
    'server_key' => env('MIDTRANS_SERVER_KEY'),
    'client_key' => env('MIDTRANS_CLIENT_KEY'),
    'is_production' => env('MIDTRANS_IS_PRODUCTION', false),
    'is_sanitized' => env('MIDTRANS_IS_SANITIZED', true),
    'is_3ds' => env('MIDTRANS_IS_3DS', true),
];

Manual Review (20%):

  • ✅ Sandbox keys untuk development
  • ⚠️ Ganti ke production keys saat deploy

Prompt 2: MidtransService

Buatkan MidtransService untuk handle:
1. Create Snap Token untuk pembayaran pendaftaran
2. Handle notification/webhook dari Midtrans
3. Verify signature dari callback
4. Update payment status di Registration

Response Claude (80%):

<?php

// app/Services/MidtransService.php

namespace App\\Services;

use App\\Models\\Registration;
use Midtrans\\Config;
use Midtrans\\Snap;
use Midtrans\\Notification;
use Illuminate\\Support\\Facades\\Log;

class MidtransService
{
    public function __construct()
    {
        Config::$serverKey = config('midtrans.server_key');
        Config::$clientKey = config('midtrans.client_key');
        Config::$isProduction = config('midtrans.is_production');
        Config::$isSanitized = config('midtrans.is_sanitized');
        Config::$is3ds = config('midtrans.is_3ds');
    }

    public function createSnapToken(Registration $registration): string
    {
        $orderId = 'PMB-' . $registration->id . '-' . time();

        $params = [
            'transaction_details' => [
                'order_id' => $orderId,
                'gross_amount' => $registration->payment_amount,
            ],
            'customer_details' => [
                'first_name' => $registration->full_name,
                'email' => $registration->user->email,
                'phone' => $registration->phone,
            ],
            'item_details' => [
                [
                    'id' => 'REG-' . $registration->admission_path_id,
                    'price' => $registration->payment_amount,
                    'quantity' => 1,
                    'name' => 'Biaya Pendaftaran ' . $registration->admissionPath->name,
                ],
            ],
            'callbacks' => [
                'finish' => route('applicant.payment.finish'),
            ],
        ];

        // Save order_id untuk tracking
        $registration->update([
            'midtrans_transaction_id' => $orderId,
            'payment_status' => 'pending',
        ]);

        return Snap::getSnapToken($params);
    }

    public function handleNotification(): array
    {
        $notification = new Notification();

        $transactionStatus = $notification->transaction_status;
        $orderId = $notification->order_id;
        $fraudStatus = $notification->fraud_status ?? null;
        $paymentType = $notification->payment_type;

        Log::info('Midtrans Notification', [
            'order_id' => $orderId,
            'status' => $transactionStatus,
            'fraud' => $fraudStatus,
            'type' => $paymentType,
        ]);

        // Find registration by order_id
        $registration = Registration::where('midtrans_transaction_id', $orderId)->first();

        if (!$registration) {
            Log::error('Registration not found for order: ' . $orderId);
            return ['status' => 'error', 'message' => 'Registration not found'];
        }

        // Determine payment status
        $paymentStatus = $this->determinePaymentStatus($transactionStatus, $fraudStatus);

        // Update registration
        $updateData = [
            'payment_status' => $paymentStatus,
            'payment_type' => $paymentType,
        ];

        if ($paymentStatus === 'paid') {
            $updateData['paid_at'] = now();
            $updateData['status'] = 'paid'; // Move to verification queue
        }

        $registration->update($updateData);

        return [
            'status' => 'success',
            'payment_status' => $paymentStatus,
            'registration_id' => $registration->id,
        ];
    }

    protected function determinePaymentStatus(string $transactionStatus, ?string $fraudStatus): string
    {
        if ($transactionStatus === 'capture') {
            return $fraudStatus === 'accept' ? 'paid' : 'pending';
        }

        return match ($transactionStatus) {
            'settlement' => 'paid',
            'pending' => 'pending',
            'deny', 'expire', 'cancel' => 'unpaid',
            default => 'pending',
        };
    }

    public function verifySignature(array $payload): bool
    {
        $orderId = $payload['order_id'];
        $statusCode = $payload['status_code'];
        $grossAmount = $payload['gross_amount'];
        $serverKey = config('midtrans.server_key');

        $signature = hash('sha512', $orderId . $statusCode . $grossAmount . $serverKey);

        return $signature === $payload['signature_key'];
    }
}

Manual Review (20%):

  • ✅ Snap token generation
  • ✅ Webhook handling
  • ✅ Signature verification
  • ✅ Status mapping

Prompt 3: Payment Page untuk Applicant

Buatkan Payment Page di Applicant Panel dengan:
1. Tampilkan summary pendaftaran
2. Tampilkan biaya yang harus dibayar
3. Tombol bayar yang trigger Midtrans Snap
4. Handle callback finish

Response Claude (80%):

<?php

// app/Filament/Applicant/Pages/Payment.php

namespace App\\Filament\\Applicant\\Pages;

use App\\Models\\Registration;
use App\\Services\\MidtransService;
use Filament\\Notifications\\Notification;
use Filament\\Pages\\Page;
use Illuminate\\Support\\Facades\\Auth;

class Payment extends Page
{
    protected static ?string $navigationIcon = 'heroicon-o-banknotes';
    protected static ?string $navigationLabel = 'Pembayaran';
    protected static ?string $title = 'Pembayaran Biaya Pendaftaran';
    protected static ?int $navigationSort = 2;

    protected static string $view = 'filament.applicant.pages.payment';

    public ?Registration $registration = null;
    public ?string $snapToken = null;
    public string $clientKey;

    public function mount(): void
    {
        $this->registration = Registration::where('user_id', Auth::id())
            ->whereIn('status', ['submitted', 'paid'])
            ->first();

        if (!$this->registration) {
            Notification::make()
                ->title('Tidak ada pendaftaran yang perlu dibayar')
                ->warning()
                ->send();

            $this->redirect(route('filament.applicant.pages.dashboard'));
            return;
        }

        // Already paid
        if ($this->registration->payment_status === 'paid') {
            $this->redirect(route('filament.applicant.pages.dashboard'));
            return;
        }

        $this->clientKey = config('midtrans.client_key');
    }

    public function getSnapToken(): void
    {
        if ($this->registration->payment_status === 'paid') {
            Notification::make()
                ->title('Pembayaran sudah berhasil')
                ->success()
                ->send();
            return;
        }

        try {
            $midtransService = new MidtransService();
            $this->snapToken = $midtransService->createSnapToken($this->registration);
        } catch (\\Exception $e) {
            Notification::make()
                ->title('Gagal membuat pembayaran')
                ->body($e->getMessage())
                ->danger()
                ->send();
        }
    }

    public static function shouldRegisterNavigation(): bool
    {
        $registration = Registration::where('user_id', Auth::id())
            ->where('status', 'submitted')
            ->where('payment_status', 'unpaid')
            ->first();

        return $registration !== null;
    }
}

Blade View untuk Payment:

{{-- resources/views/filament/applicant/pages/payment.blade.php --}}

<x-filament-panels::page>
    @if($this->registration)
        <div class="max-w-2xl mx-auto">
            {{-- Summary Card --}}
            <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden mb-6">
                <div class="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4">
                    <h2 class="text-xl font-bold text-white">Ringkasan Pendaftaran</h2>
                </div>

                <div class="p-6">
                    <dl class="space-y-4">
                        <div class="flex justify-between py-2 border-b">
                            <dt class="text-gray-500">No. Registrasi</dt>
                            <dd class="font-semibold">{{ $this->registration->registration_number }}</dd>
                        </div>
                        <div class="flex justify-between py-2 border-b">
                            <dt class="text-gray-500">Nama</dt>
                            <dd class="font-semibold">{{ $this->registration->full_name }}</dd>
                        </div>
                        <div class="flex justify-between py-2 border-b">
                            <dt class="text-gray-500">Gelombang</dt>
                            <dd>{{ $this->registration->admissionWave->full_name }}</dd>
                        </div>
                        <div class="flex justify-between py-2 border-b">
                            <dt class="text-gray-500">Jalur</dt>
                            <dd>{{ $this->registration->admissionPath->name }}</dd>
                        </div>
                        <div class="flex justify-between py-2 border-b">
                            <dt class="text-gray-500">Pilihan 1</dt>
                            <dd>{{ $this->registration->firstChoice->name }}</dd>
                        </div>
                    </dl>
                </div>
            </div>

            {{-- Payment Card --}}
            <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
                <div class="p-6">
                    <div class="text-center mb-6">
                        <p class="text-gray-500 mb-2">Total Biaya Pendaftaran</p>
                        <p class="text-4xl font-bold text-primary-600">
                            {{ $this->registration->formatted_payment_amount }}
                        </p>
                    </div>

                    @if($this->registration->payment_status === 'pending')
                        <div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4 mb-6">
                            <div class="flex items-center gap-3">
                                <x-heroicon-o-clock class="w-6 h-6 text-yellow-600" />
                                <div>
                                    <p class="font-semibold text-yellow-800 dark:text-yellow-200">
                                        Menunggu Konfirmasi Pembayaran
                                    </p>
                                    <p class="text-sm text-yellow-600">
                                        Pembayaran Anda sedang diproses. Halaman akan refresh otomatis.
                                    </p>
                                </div>
                            </div>
                        </div>
                    @endif

                    <button
                        wire:click="getSnapToken"
                        id="pay-button"
                        class="w-full py-4 px-6 bg-green-600 hover:bg-green-700 text-white font-bold rounded-lg transition flex items-center justify-center gap-2"
                    >
                        <x-heroicon-o-credit-card class="w-6 h-6" />
                        Bayar Sekarang
                    </button>

                    <p class="text-center text-sm text-gray-500 mt-4">
                        Pembayaran aman melalui Midtrans
                    </p>

                    {{-- Payment Methods --}}
                    <div class="mt-6 pt-6 border-t">
                        <p class="text-sm text-gray-500 text-center mb-4">Metode pembayaran tersedia:</p>
                        <div class="flex flex-wrap justify-center gap-4 text-xs text-gray-600">
                            <span>💳 Kartu Kredit/Debit</span>
                            <span>🏦 Transfer Bank</span>
                            <span>📱 GoPay</span>
                            <span>💰 OVO</span>
                            <span>🏪 Indomaret/Alfamart</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    @endif

    {{-- Midtrans Snap JS --}}
    @if($this->snapToken)
        <script src="<https://app.sandbox.midtrans.com/snap/snap.js>" data-client-key="{{ $this->clientKey }}"></script>
        <script>
            window.snap.pay('{{ $this->snapToken }}', {
                onSuccess: function(result) {
                    window.location.href = '{{ route("applicant.payment.finish") }}?status=success';
                },
                onPending: function(result) {
                    window.location.href = '{{ route("applicant.payment.finish") }}?status=pending';
                },
                onError: function(result) {
                    window.location.href = '{{ route("applicant.payment.finish") }}?status=error';
                },
                onClose: function() {
                    // User closed popup without finishing
                }
            });
        </script>
    @endif
</x-filament-panels::page>

Manual Review (20%):

  • ✅ Summary card
  • ✅ Snap popup trigger
  • ✅ Callback handlers
  • ⚠️ Ganti sandbox URL ke production saat deploy

Prompt 4: Webhook Controller

Buatkan controller untuk handle Midtrans webhook notification.
Endpoint harus accessible tanpa auth (dari server Midtrans).

Response Claude (80%):

<?php

// app/Http/Controllers/MidtransWebhookController.php

namespace App\\Http\\Controllers;

use App\\Services\\MidtransService;
use Illuminate\\Http\\Request;
use Illuminate\\Http\\JsonResponse;
use Illuminate\\Support\\Facades\\Log;

class MidtransWebhookController extends Controller
{
    public function handle(Request $request): JsonResponse
    {
        try {
            $midtransService = new MidtransService();

            // Verify signature
            $payload = $request->all();

            if (!$midtransService->verifySignature($payload)) {
                Log::warning('Invalid Midtrans signature', $payload);
                return response()->json(['status' => 'error', 'message' => 'Invalid signature'], 403);
            }

            // Handle notification
            $result = $midtransService->handleNotification();

            return response()->json($result);

        } catch (\\Exception $e) {
            Log::error('Midtrans webhook error: ' . $e->getMessage());
            return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);
        }
    }
}
// routes/web.php - tambahkan route

use App\\Http\\Controllers\\MidtransWebhookController;

Route::post('/midtrans/webhook', [MidtransWebhookController::class, 'handle'])
    ->name('midtrans.webhook')
    ->withoutMiddleware([\\App\\Http\\Middleware\\VerifyCsrfToken::class]);
// app/Http/Controllers/PaymentCallbackController.php

namespace App\\Http\\Controllers;

use App\\Models\\Registration;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Auth;

class PaymentCallbackController extends Controller
{
    public function finish(Request $request)
    {
        $status = $request->get('status', 'unknown');

        $registration = Registration::where('user_id', Auth::id())->first();

        if ($status === 'success' && $registration) {
            // Refresh dari database
            $registration->refresh();
        }

        return redirect()->route('filament.applicant.pages.dashboard')
            ->with('payment_status', $status);
    }
}
// routes/web.php - tambahkan route callback

Route::get('/payment/finish', [PaymentCallbackController::class, 'finish'])
    ->name('applicant.payment.finish')
    ->middleware(['auth']);

Manual Review (20%):

  • ✅ CSRF exemption untuk webhook
  • ✅ Signature verification
  • ✅ Callback redirect

Prompt 5: Install DomPDF

Install DomPDF untuk generate kartu ujian PDF.

Response Claude (80%):

composer require barryvdh/laravel-dompdf
// config/dompdf.php akan auto-publish, atau jalankan:
php artisan vendor:publish --provider="Barryvdh\\DomPDF\\ServiceProvider"

Manual Review (20%):

  • ✅ Package standard untuk Laravel PDF

Prompt 6: ExamCard PDF Generator

Buatkan service untuk generate kartu ujian PDF dengan:
- Header: Logo kampus, judul "KARTU PESERTA UJIAN"
- Foto pendaftar
- Data: nomor peserta, nama, jalur, pilihan prodi
- Jadwal ujian: tanggal, waktu, ruangan, nomor kursi
- QR Code berisi nomor peserta
- Tanda tangan panitia
- Ukuran A5 landscape

Response Claude (80%):

<?php

// app/Services/ExamCardService.php

namespace App\\Services;

use App\\Models\\Registration;
use App\\Models\\ExamSchedule;
use Barryvdh\\DomPDF\\Facade\\Pdf;
use Illuminate\\Support\\Facades\\Storage;
use SimpleSoftwareIO\\QrCode\\Facades\\QrCode;

class ExamCardService
{
    public function generate(Registration $registration)
    {
        // Get exam schedule
        $examSchedule = ExamSchedule::where('admission_wave_id', $registration->admission_wave_id)
            ->where('exam_type', 'written')
            ->first();

        // Generate QR Code (base64)
        $qrCode = base64_encode(
            QrCode::format('svg')
                ->size(100)
                ->generate($registration->exam_number)
        );

        // Get photo as base64
        $photoBase64 = null;
        if ($registration->photo) {
            $photoPath = Storage::path($registration->photo);
            if (file_exists($photoPath)) {
                $photoBase64 = base64_encode(file_get_contents($photoPath));
                $photoMime = mime_content_type($photoPath);
            }
        }

        $data = [
            'registration' => $registration,
            'examSchedule' => $examSchedule,
            'qrCode' => $qrCode,
            'photoBase64' => $photoBase64,
            'photoMime' => $photoMime ?? 'image/jpeg',
        ];

        $pdf = Pdf::loadView('pdf.exam-card', $data);
        $pdf->setPaper('a5', 'landscape');

        return $pdf;
    }

    public function download(Registration $registration)
    {
        $pdf = $this->generate($registration);
        $filename = "kartu-ujian-{$registration->exam_number}.pdf";

        return $pdf->download($filename);
    }

    public function stream(Registration $registration)
    {
        $pdf = $this->generate($registration);

        return $pdf->stream("kartu-ujian-{$registration->exam_number}.pdf");
    }
}

Install QR Code package:

composer require simplesoftwareio/simple-qrcode

Blade Template untuk Kartu Ujian:

{{-- resources/views/pdf/exam-card.blade.php --}}

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Kartu Peserta Ujian - {{ $registration->exam_number }}</title>
    <style>
        @page {
            margin: 0;
        }

        body {
            font-family: 'Helvetica', sans-serif;
            font-size: 11px;
            margin: 0;
            padding: 15px;
            background: #fff;
        }

        .card {
            border: 3px solid #1a365d;
            border-radius: 8px;
            padding: 15px;
            height: calc(100% - 30px);
        }

        .header {
            text-align: center;
            border-bottom: 2px solid #1a365d;
            padding-bottom: 10px;
            margin-bottom: 15px;
        }

        .header h1 {
            margin: 0;
            font-size: 16px;
            color: #1a365d;
            text-transform: uppercase;
            letter-spacing: 2px;
        }

        .header h2 {
            margin: 5px 0 0;
            font-size: 12px;
            color: #4a5568;
            font-weight: normal;
        }

        .content {
            display: table;
            width: 100%;
        }

        .left-section {
            display: table-cell;
            width: 25%;
            vertical-align: top;
            text-align: center;
            padding-right: 15px;
        }

        .photo-frame {
            border: 2px solid #cbd5e0;
            padding: 3px;
            background: #f7fafc;
            margin-bottom: 10px;
        }

        .photo-frame img {
            width: 90px;
            height: 120px;
            object-fit: cover;
        }

        .exam-number {
            background: #1a365d;
            color: white;
            padding: 8px;
            font-size: 14px;
            font-weight: bold;
            border-radius: 4px;
            margin-top: 10px;
        }

        .right-section {
            display: table-cell;
            width: 75%;
            vertical-align: top;
        }

        .info-grid {
            width: 100%;
        }

        .info-row {
            margin-bottom: 8px;
        }

        .info-label {
            color: #718096;
            font-size: 10px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .info-value {
            font-weight: bold;
            font-size: 12px;
            color: #2d3748;
        }

        .schedule-box {
            background: #ebf8ff;
            border: 1px solid #90cdf4;
            border-radius: 6px;
            padding: 12px;
            margin-top: 15px;
        }

        .schedule-box h3 {
            margin: 0 0 10px;
            font-size: 12px;
            color: #2b6cb0;
            text-transform: uppercase;
        }

        .schedule-grid {
            display: table;
            width: 100%;
        }

        .schedule-item {
            display: table-cell;
            text-align: center;
            padding: 5px;
        }

        .schedule-item .label {
            font-size: 9px;
            color: #4a5568;
        }

        .schedule-item .value {
            font-size: 13px;
            font-weight: bold;
            color: #2d3748;
        }

        .footer {
            margin-top: 15px;
            display: table;
            width: 100%;
            border-top: 1px dashed #cbd5e0;
            padding-top: 10px;
        }

        .qr-section {
            display: table-cell;
            width: 20%;
            vertical-align: middle;
        }

        .signature-section {
            display: table-cell;
            width: 40%;
            text-align: center;
            vertical-align: bottom;
        }

        .signature-line {
            border-bottom: 1px solid #2d3748;
            width: 150px;
            margin: 30px auto 5px;
        }

        .notes-section {
            display: table-cell;
            width: 40%;
            vertical-align: top;
            font-size: 8px;
            color: #718096;
        }

        .notes-section ul {
            margin: 0;
            padding-left: 15px;
        }

        .watermark {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) rotate(-45deg);
            font-size: 60px;
            color: rgba(0, 0, 0, 0.03);
            text-transform: uppercase;
            letter-spacing: 10px;
            z-index: -1;
        }
    </style>
</head>
<body>
    <div class="watermark">UNIVERSITAS NUSANTARA</div>

    <div class="card">
        {{-- Header --}}
        <div class="header">
            <h1>Kartu Peserta Ujian</h1>
            <h2>Penerimaan Mahasiswa Baru {{ $registration->admissionWave->year }}</h2>
        </div>

        {{-- Content --}}
        <div class="content">
            {{-- Left: Photo & Exam Number --}}
            <div class="left-section">
                <div class="photo-frame">
                    @if($photoBase64)
                        <img src="data:{{ $photoMime }};base64,{{ $photoBase64 }}" alt="Foto">
                    @else
                        <div style="width:90px;height:120px;background:#e2e8f0;display:flex;align-items:center;justify-content:center;">
                            <span style="color:#a0aec0;">No Photo</span>
                        </div>
                    @endif
                </div>

                <div class="exam-number">
                    {{ $registration->exam_number }}
                </div>
            </div>

            {{-- Right: Info --}}
            <div class="right-section">
                <div class="info-grid">
                    <div class="info-row">
                        <div class="info-label">Nama Lengkap</div>
                        <div class="info-value">{{ strtoupper($registration->full_name) }}</div>
                    </div>

                    <div style="display:table;width:100%;">
                        <div style="display:table-cell;width:50%;">
                            <div class="info-row">
                                <div class="info-label">Tempat, Tanggal Lahir</div>
                                <div class="info-value">{{ $registration->birth_info }}</div>
                            </div>
                        </div>
                        <div style="display:table-cell;width:50%;">
                            <div class="info-row">
                                <div class="info-label">Jenis Kelamin</div>
                                <div class="info-value">{{ $registration->gender_label }}</div>
                            </div>
                        </div>
                    </div>

                    <div class="info-row">
                        <div class="info-label">Jalur Pendaftaran</div>
                        <div class="info-value">{{ $registration->admissionPath->name }}</div>
                    </div>

                    <div style="display:table;width:100%;">
                        <div style="display:table-cell;width:50%;">
                            <div class="info-row">
                                <div class="info-label">Pilihan 1</div>
                                <div class="info-value">{{ $registration->firstChoice->name }}</div>
                            </div>
                        </div>
                        <div style="display:table-cell;width:50%;">
                            <div class="info-row">
                                <div class="info-label">Pilihan 2</div>
                                <div class="info-value">{{ $registration->secondChoice?->name ?? '-' }}</div>
                            </div>
                        </div>
                    </div>
                </div>

                {{-- Schedule Box --}}
                @if($examSchedule)
                <div class="schedule-box">
                    <h3>🗓 Jadwal Ujian</h3>
                    <div class="schedule-grid">
                        <div class="schedule-item">
                            <div class="label">Tanggal</div>
                            <div class="value">{{ $examSchedule->exam_date->format('d M Y') }}</div>
                        </div>
                        <div class="schedule-item">
                            <div class="label">Waktu</div>
                            <div class="value">{{ $examSchedule->time_range }} WIB</div>
                        </div>
                        <div class="schedule-item">
                            <div class="label">Ruangan</div>
                            <div class="value">{{ $registration->exam_room ?? 'TBA' }}</div>
                        </div>
                        <div class="schedule-item">
                            <div class="label">No. Kursi</div>
                            <div class="value">{{ $registration->exam_seat ?? '-' }}</div>
                        </div>
                    </div>
                </div>
                @endif
            </div>
        </div>

        {{-- Footer --}}
        <div class="footer">
            <div class="qr-section">
                <img src="data:image/svg+xml;base64,{{ $qrCode }}" width="70" height="70">
            </div>

            <div class="notes-section">
                <strong>Ketentuan:</strong>
                <ul>
                    <li>Kartu ini wajib dibawa saat ujian</li>
                    <li>Hadir 30 menit sebelum ujian dimulai</li>
                    <li>Membawa alat tulis sendiri</li>
                    <li>Tidak diperkenankan membawa HP ke ruang ujian</li>
                </ul>
            </div>

            <div class="signature-section">
                <div class="info-label">Ketua Panitia PMB</div>
                <div class="signature-line"></div>
                <div style="font-size:10px;">( ............................ )</div>
            </div>
        </div>
    </div>
</body>
</html>

Manual Review (20%):

  • ✅ Layout A5 landscape
  • ✅ Photo embed base64
  • ✅ QR Code dengan nomor peserta
  • ✅ Jadwal ujian dinamis
  • ⚠️ Logo kampus perlu ditambahkan

Prompt 7: Route & Controller untuk Print Kartu Ujian

Buatkan route dan controller untuk print/download kartu ujian.
Hanya bisa diakses oleh pendaftar yang sudah verified dan punya exam_number.

Response Claude (80%):

<?php

// app/Http/Controllers/ExamCardController.php

namespace App\\Http\\Controllers;

use App\\Models\\Registration;
use App\\Services\\ExamCardService;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Auth;

class ExamCardController extends Controller
{
    public function __construct(
        protected ExamCardService $examCardService
    ) {}

    public function print(Registration $registration)
    {
        // Authorization check
        if ($registration->user_id !== Auth::id()) {
            abort(403, 'Unauthorized');
        }

        // Check if can print
        if (!$registration->canPrintExamCard()) {
            abort(403, 'Kartu ujian belum tersedia');
        }

        return $this->examCardService->stream($registration);
    }

    public function download(Registration $registration)
    {
        // Authorization check
        if ($registration->user_id !== Auth::id()) {
            abort(403, 'Unauthorized');
        }

        // Check if can print
        if (!$registration->canPrintExamCard()) {
            abort(403, 'Kartu ujian belum tersedia');
        }

        return $this->examCardService->download($registration);
    }

    // Admin version
    public function adminPrint(Registration $registration)
    {
        if (!$registration->canPrintExamCard()) {
            abort(403, 'Kartu ujian belum tersedia');
        }

        return $this->examCardService->stream($registration);
    }
}
// routes/web.php

use App\\Http\\Controllers\\ExamCardController;

// Applicant routes
Route::middleware(['auth'])->group(function () {
    Route::get('/exam-card/{registration}/print', [ExamCardController::class, 'print'])
        ->name('applicant.exam-card.print');
    Route::get('/exam-card/{registration}/download', [ExamCardController::class, 'download'])
        ->name('applicant.exam-card.download');
});

// Admin routes
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
    Route::get('/exam-card/{registration}/print', [ExamCardController::class, 'adminPrint'])
        ->name('exam-card.print');
});

Manual Review (20%):

  • ✅ Authorization check
  • ✅ canPrintExamCard validation
  • ✅ Stream vs download options
💡 RECAP PAYMENT & EXAM CARD:

Prompts: 7
Services created:
- MidtransService (snap token, webhook, signature verify)
- ExamCardService (PDF generation)

Pages created:
- Payment (Midtrans Snap integration)

Controllers:
- MidtransWebhookController
- PaymentCallbackController
- ExamCardController

PDF Template:
- Kartu ujian A5 landscape
- Photo, QR code, jadwal ujian
- Signature area

Features:
- Snap popup payment
- Multiple payment methods
- Webhook notification handling
- Signature verification
- PDF dengan foto pendaftar
- QR code untuk verifikasi

Yang di-review manual:
- Midtrans credentials
- Webhook URL configuration
- Production vs sandbox
- Logo kampus di PDF

Di bagian selanjutnya, kita akan buat halaman pengumuman kelulusan dan flow daftar ulang.

Bagian 9: Pengumuman Kelulusan & Daftar Ulang

Di bagian ini kita akan buat halaman pengumuman kelulusan yang bisa diakses publik dan flow daftar ulang untuk pendaftar yang diterima.

Prompt 1: Public Announcement Page

Buatkan halaman pengumuman kelulusan yang bisa diakses
tanpa login dengan:
- Input nomor peserta + tanggal lahir (verifikasi)
- Tampilkan hasil: LULUS / TIDAK LULUS / CADANGAN
- Jika lulus, tampilkan program studi diterima dan instruksi daftar ulang
- Design yang celebratory untuk yang lulus

Response Claude (80%):

<?php

// app/Http/Controllers/AnnouncementController.php

namespace App\\Http\\Controllers;

use App\\Models\\Registration;
use App\\Models\\AdmissionWave;
use Illuminate\\Http\\Request;

class AnnouncementController extends Controller
{
    public function index()
    {
        $activeWaves = AdmissionWave::where('is_active', true)
            ->whereIn('status', ['announced', 'completed'])
            ->orderByDesc('year')
            ->get();

        return view('announcement.index', compact('activeWaves'));
    }

    public function check(Request $request)
    {
        $request->validate([
            'exam_number' => 'required|string',
            'birth_date' => 'required|date',
        ]);

        $registration = Registration::where('exam_number', $request->exam_number)
            ->whereDate('birth_date', $request->birth_date)
            ->whereIn('status', ['announced', 'accepted', 'rejected', 're_registered'])
            ->first();

        if (!$registration) {
            return back()->withErrors([
                'exam_number' => 'Data tidak ditemukan. Pastikan nomor peserta dan tanggal lahir benar.'
            ])->withInput();
        }

        return view('announcement.result', compact('registration'));
    }
}
// routes/web.php

use App\\Http\\Controllers\\AnnouncementController;

Route::get('/pengumuman', [AnnouncementController::class, 'index'])
    ->name('announcement.index');
Route::post('/pengumuman/check', [AnnouncementController::class, 'check'])
    ->name('announcement.check');

Blade View — Form Pencarian:

{{-- resources/views/announcement/index.blade.php --}}

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pengumuman Kelulusan - PMB Universitas Nusantara</title>
    <script src="<https://cdn.tailwindcss.com>"></script>
</head>
<body class="bg-gradient-to-br from-blue-900 via-blue-800 to-indigo-900 min-h-screen">
    <div class="container mx-auto px-4 py-12">
        {{-- Header --}}
        <div class="text-center mb-12">
            <h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
                🎓 Pengumuman Kelulusan
            </h1>
            <p class="text-blue-200 text-lg">
                Penerimaan Mahasiswa Baru Universitas Nusantara
            </p>
        </div>

        {{-- Search Card --}}
        <div class="max-w-md mx-auto">
            <div class="bg-white rounded-2xl shadow-2xl p-8">
                <h2 class="text-xl font-semibold text-gray-800 mb-6 text-center">
                    Cek Hasil Seleksi
                </h2>

                @if($errors->any())
                    <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
                        <p class="text-red-600 text-sm">{{ $errors->first() }}</p>
                    </div>
                @endif

                <form action="{{ route('announcement.check') }}" method="POST">
                    @csrf

                    <div class="mb-4">
                        <label class="block text-gray-700 text-sm font-medium mb-2">
                            Nomor Peserta
                        </label>
                        <input
                            type="text"
                            name="exam_number"
                            value="{{ old('exam_number') }}"
                            placeholder="Contoh: 2026-01-REG-00001"
                            class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                            required
                        >
                    </div>

                    <div class="mb-6">
                        <label class="block text-gray-700 text-sm font-medium mb-2">
                            Tanggal Lahir
                        </label>
                        <input
                            type="date"
                            name="birth_date"
                            value="{{ old('birth_date') }}"
                            class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                            required
                        >
                    </div>

                    <button
                        type="submit"
                        class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200"
                    >
                        Cek Hasil
                    </button>
                </form>

                <p class="text-center text-gray-500 text-sm mt-6">
                    Tanggal lahir digunakan untuk verifikasi identitas
                </p>
            </div>

            {{-- Active Waves Info --}}
            @if($activeWaves->isNotEmpty())
                <div class="mt-8 bg-white/10 backdrop-blur rounded-xl p-6">
                    <h3 class="text-white font-semibold mb-4">Gelombang yang Sudah Diumumkan:</h3>
                    <ul class="space-y-2">
                        @foreach($activeWaves as $wave)
                            <li class="text-blue-200 flex items-center gap-2">
                                <span class="text-green-400">✓</span>
                                {{ $wave->full_name }} — Diumumkan {{ $wave->announcement_date?->format('d M Y') }}
                            </li>
                        @endforeach
                    </ul>
                </div>
            @endif
        </div>
    </div>
</body>
</html>

Blade View — Hasil Pengumuman:

{{-- resources/views/announcement/result.blade.php --}}

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hasil Seleksi - {{ $registration->full_name }}</title>
    <script src="<https://cdn.tailwindcss.com>"></script>
    @if($registration->result === 'passed')
        <script src="<https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js>"></script>
    @endif
</head>
<body class="min-h-screen {{ $registration->result === 'passed' ? 'bg-gradient-to-br from-green-600 via-emerald-600 to-teal-700' : ($registration->result === 'waitlist' ? 'bg-gradient-to-br from-yellow-600 via-amber-600 to-orange-600' : 'bg-gradient-to-br from-gray-700 via-gray-800 to-gray-900') }}">

    <div class="container mx-auto px-4 py-12">
        <div class="max-w-2xl mx-auto">

            {{-- Result Card --}}
            <div class="bg-white rounded-3xl shadow-2xl overflow-hidden">

                {{-- Header --}}
                <div class="p-8 text-center {{ $registration->result === 'passed' ? 'bg-green-50' : ($registration->result === 'waitlist' ? 'bg-yellow-50' : 'bg-gray-50') }}">

                    @if($registration->result === 'passed')
                        <div class="text-6xl mb-4">🎉</div>
                        <h1 class="text-3xl font-bold text-green-700 mb-2">SELAMAT!</h1>
                        <p class="text-green-600 text-lg">Anda dinyatakan</p>
                        <div class="inline-block bg-green-600 text-white text-2xl font-bold px-8 py-3 rounded-full mt-4">
                            LULUS
                        </div>
                    @elseif($registration->result === 'waitlist')
                        <div class="text-6xl mb-4">📋</div>
                        <h1 class="text-3xl font-bold text-yellow-700 mb-2">CADANGAN</h1>
                        <p class="text-yellow-600 text-lg">Anda masuk dalam daftar cadangan</p>
                        <div class="inline-block bg-yellow-600 text-white text-xl font-bold px-8 py-3 rounded-full mt-4">
                            DAFTAR CADANGAN
                        </div>
                    @else
                        <div class="text-6xl mb-4">📄</div>
                        <h1 class="text-3xl font-bold text-gray-700 mb-2">PENGUMUMAN</h1>
                        <p class="text-gray-600 text-lg">Hasil Seleksi PMB</p>
                        <div class="inline-block bg-gray-600 text-white text-xl font-bold px-8 py-3 rounded-full mt-4">
                            TIDAK LULUS
                        </div>
                    @endif
                </div>

                {{-- Details --}}
                <div class="p-8">
                    <div class="grid grid-cols-2 gap-6 mb-8">
                        <div>
                            <p class="text-gray-500 text-sm">Nomor Peserta</p>
                            <p class="font-semibold text-gray-800">{{ $registration->exam_number }}</p>
                        </div>
                        <div>
                            <p class="text-gray-500 text-sm">Nama</p>
                            <p class="font-semibold text-gray-800">{{ $registration->full_name }}</p>
                        </div>
                        <div>
                            <p class="text-gray-500 text-sm">Jalur</p>
                            <p class="font-semibold text-gray-800">{{ $registration->admissionPath->name }}</p>
                        </div>
                        <div>
                            <p class="text-gray-500 text-sm">Nilai Akhir</p>
                            <p class="font-semibold text-gray-800">{{ $registration->formatted_final_score }}</p>
                        </div>
                    </div>

                    @if($registration->result === 'passed')
                        {{-- Accepted Program --}}
                        <div class="bg-green-50 border-2 border-green-200 rounded-xl p-6 mb-8">
                            <h3 class="text-green-800 font-semibold mb-2">Diterima di Program Studi:</h3>
                            <p class="text-2xl font-bold text-green-700">
                                {{ $registration->acceptedProgram->degree }} {{ $registration->acceptedProgram->name }}
                            </p>
                            <p class="text-green-600 mt-1">
                                {{ $registration->acceptedProgram->faculty->name }}
                            </p>
                        </div>

                        {{-- Re-registration Info --}}
                        <div class="bg-blue-50 border border-blue-200 rounded-xl p-6">
                            <h3 class="text-blue-800 font-semibold mb-4 flex items-center gap-2">
                                <span>📝</span> Langkah Selanjutnya: Daftar Ulang
                            </h3>

                            <div class="space-y-3 text-sm text-blue-700">
                                <p>
                                    <strong>Batas Waktu:</strong>
                                    {{ $registration->admissionWave->re_registration_deadline?->format('d F Y') }}
                                </p>
                                <p>
                                    <strong>Biaya UKT:</strong>
                                    {{ $registration->acceptedProgram->formatted_tuition }} / semester
                                </p>
                            </div>

                            <div class="mt-6">
                                <a
                                    href="{{ route('filament.applicant.pages.dashboard') }}"
                                    class="inline-block bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-lg transition"
                                >
                                    Login untuk Daftar Ulang →
                                </a>
                            </div>
                        </div>

                    @elseif($registration->result === 'waitlist')
                        <div class="bg-yellow-50 border border-yellow-200 rounded-xl p-6">
                            <h3 class="text-yellow-800 font-semibold mb-2">Informasi Cadangan</h3>
                            <p class="text-yellow-700 text-sm">
                                Anda berada di posisi cadangan. Jika ada peserta yang lulus tidak melakukan
                                daftar ulang hingga batas waktu, Anda berkesempatan untuk dipanggil.
                                Pantau terus email dan pengumuman di website.
                            </p>
                        </div>

                    @else
                        <div class="bg-gray-50 border border-gray-200 rounded-xl p-6">
                            <h3 class="text-gray-800 font-semibold mb-2">Jangan Menyerah!</h3>
                            <p class="text-gray-600 text-sm">
                                Terima kasih telah berpartisipasi dalam seleksi PMB Universitas Nusantara.
                                Anda dapat mencoba kembali di gelombang berikutnya atau tahun depan.
                                Tetap semangat!
                            </p>
                        </div>
                    @endif
                </div>

                {{-- Footer --}}
                <div class="bg-gray-50 px-8 py-4 border-t">
                    <a href="{{ route('announcement.index') }}" class="text-blue-600 hover:underline text-sm">
                        ← Cek peserta lain
                    </a>
                </div>
            </div>
        </div>
    </div>

    @if($registration->result === 'passed')
        <script>
            // Confetti celebration!
            confetti({
                particleCount: 100,
                spread: 70,
                origin: { y: 0.6 }
            });

            setTimeout(() => {
                confetti({
                    particleCount: 50,
                    angle: 60,
                    spread: 55,
                    origin: { x: 0 }
                });
                confetti({
                    particleCount: 50,
                    angle: 120,
                    spread: 55,
                    origin: { x: 1 }
                });
            }, 500);
        </script>
    @endif
</body>
</html>

Manual Review (20%):

  • ✅ Verifikasi dengan tanggal lahir
  • ✅ Confetti untuk yang lulus
  • ✅ Different styling per result
  • ✅ Re-registration info untuk yang lulus

Prompt 2: Re-Registration Page

Buatkan halaman daftar ulang di Applicant Panel dengan:
- Tampilkan info kelulusan dan program diterima
- Upload dokumen tambahan jika diperlukan
- Pembayaran UKT via Midtrans
- Generate NIM setelah daftar ulang complete

Response Claude (80%):

<?php

// app/Filament/Applicant/Pages/ReRegistration.php

namespace App\\Filament\\Applicant\\Pages;

use App\\Models\\Registration;
use App\\Services\\MidtransService;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Forms\\Concerns\\InteractsWithForms;
use Filament\\Forms\\Contracts\\HasForms;
use Filament\\Notifications\\Notification;
use Filament\\Pages\\Page;
use Illuminate\\Support\\Facades\\Auth;
use Illuminate\\Support\\Str;

class ReRegistration extends Page implements HasForms
{
    use InteractsWithForms;

    protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
    protected static ?string $navigationLabel = 'Daftar Ulang';
    protected static ?string $title = 'Daftar Ulang Mahasiswa Baru';
    protected static ?int $navigationSort = 3;

    protected static string $view = 'filament.applicant.pages.re-registration';

    public ?Registration $registration = null;
    public ?array $data = [];
    public ?string $snapToken = null;
    public string $clientKey;

    public function mount(): void
    {
        $this->registration = Registration::where('user_id', Auth::id())
            ->where('status', 'accepted')
            ->where('result', 'passed')
            ->first();

        if (!$this->registration) {
            Notification::make()
                ->title('Anda tidak memiliki akses ke halaman ini')
                ->warning()
                ->send();

            $this->redirect(route('filament.applicant.pages.dashboard'));
            return;
        }

        // Already completed
        if ($this->registration->re_registration_status === 'completed') {
            $this->redirect(route('filament.applicant.pages.dashboard'));
            return;
        }

        $this->clientKey = config('midtrans.client_key');

        // Set UKT amount from accepted program
        if (!$this->registration->ukt_amount) {
            $this->registration->update([
                'ukt_amount' => $this->registration->acceptedProgram->tuition_fee,
                're_registration_status' => 'pending',
            ]);
        }
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Dokumen Tambahan')
                    ->description('Upload dokumen tambahan jika diperlukan')
                    ->schema([
                        Forms\\Components\\FileUpload::make('additional_documents')
                            ->label('Dokumen Tambahan')
                            ->multiple()
                            ->maxFiles(5)
                            ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf'])
                            ->maxSize(2048)
                            ->directory('registrations/additional')
                            ->helperText('Opsional. Upload jika ada dokumen yang diminta panitia.'),
                    ]),
            ])
            ->statePath('data');
    }

    public function payUkt(): void
    {
        if ($this->registration->ukt_paid_at) {
            Notification::make()
                ->title('UKT sudah dibayar')
                ->warning()
                ->send();
            return;
        }

        try {
            $midtransService = new MidtransService();
            $this->snapToken = $midtransService->createUktSnapToken($this->registration);
        } catch (\\Exception $e) {
            Notification::make()
                ->title('Gagal membuat pembayaran')
                ->body($e->getMessage())
                ->danger()
                ->send();
        }
    }

    public function completeReRegistration(): void
    {
        if (!$this->registration->ukt_paid_at) {
            Notification::make()
                ->title('UKT belum dibayar')
                ->danger()
                ->send();
            return;
        }

        // Generate NIM
        $nim = $this->generateNim();

        $this->registration->update([
            'nim' => $nim,
            'status' => 're_registered',
            're_registration_status' => 'completed',
            're_registration_completed_at' => now(),
        ]);

        Notification::make()
            ->title('Selamat! Daftar Ulang Berhasil')
            ->body("NIM Anda: {$nim}")
            ->success()
            ->send();

        $this->redirect(route('filament.applicant.pages.dashboard'));
    }

    protected function generateNim(): string
    {
        $year = date('Y');
        $facultyCode = $this->registration->acceptedProgram->faculty->code;
        $programCode = $this->registration->acceptedProgram->code;

        // Get next sequence
        $lastNim = Registration::where('nim', 'like', "{$year}{$programCode}%")
            ->orderByDesc('nim')
            ->value('nim');

        if ($lastNim) {
            $lastSeq = (int) substr($lastNim, -4);
            $nextSeq = $lastSeq + 1;
        } else {
            $nextSeq = 1;
        }

        return $year . $programCode . Str::padLeft($nextSeq, 4, '0');
    }

    public static function shouldRegisterNavigation(): bool
    {
        $registration = Registration::where('user_id', Auth::id())
            ->where('status', 'accepted')
            ->where('result', 'passed')
            ->where(function ($q) {
                $q->whereNull('re_registration_status')
                  ->orWhere('re_registration_status', 'pending');
            })
            ->first();

        return $registration !== null;
    }
}

Tambahan method di MidtransService untuk UKT:

// app/Services/MidtransService.php - tambahkan method

public function createUktSnapToken(Registration $registration): string
{
    $orderId = 'UKT-' . $registration->id . '-' . time();

    $params = [
        'transaction_details' => [
            'order_id' => $orderId,
            'gross_amount' => $registration->ukt_amount,
        ],
        'customer_details' => [
            'first_name' => $registration->full_name,
            'email' => $registration->user->email,
            'phone' => $registration->phone,
        ],
        'item_details' => [
            [
                'id' => 'UKT-' . $registration->accepted_program_id,
                'price' => $registration->ukt_amount,
                'quantity' => 1,
                'name' => 'UKT Semester 1 - ' . $registration->acceptedProgram->name,
            ],
        ],
        'callbacks' => [
            'finish' => route('applicant.ukt.finish'),
        ],
    ];

    return Snap::getSnapToken($params);
}

public function handleUktNotification(): array
{
    $notification = new Notification();

    $transactionStatus = $notification->transaction_status;
    $orderId = $notification->order_id;
    $paymentType = $notification->payment_type;

    // Check if UKT payment
    if (!str_starts_with($orderId, 'UKT-')) {
        return $this->handleNotification(); // Regular payment
    }

    $registrationId = explode('-', $orderId)[1];
    $registration = Registration::find($registrationId);

    if (!$registration) {
        return ['status' => 'error', 'message' => 'Registration not found'];
    }

    if (in_array($transactionStatus, ['settlement', 'capture'])) {
        $registration->update([
            'ukt_paid_at' => now(),
        ]);
    }

    return [
        'status' => 'success',
        'type' => 'ukt',
        'registration_id' => $registration->id,
    ];
}

Blade View untuk Re-Registration:

{{-- resources/views/filament/applicant/pages/re-registration.blade.php --}}

<x-filament-panels::page>
    @if($this->registration)
        <div class="max-w-3xl mx-auto space-y-6">

            {{-- Congratulations Banner --}}
            <div class="bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl p-8 text-white text-center">
                <div class="text-5xl mb-4">🎓</div>
                <h2 class="text-2xl font-bold mb-2">Selamat, Anda Diterima!</h2>
                <p class="text-green-100">
                    {{ $this->registration->acceptedProgram->degree }}
                    {{ $this->registration->acceptedProgram->name }}
                </p>
            </div>

            {{-- Status Progress --}}
            <div class="bg-white dark:bg-gray-800 rounded-xl shadow p-6">
                <h3 class="font-semibold mb-4">Progress Daftar Ulang</h3>

                <div class="flex items-center gap-4">
                    {{-- Step 1: Lulus --}}
                    <div class="flex items-center gap-2">
                        <div class="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center">
                            <x-heroicon-o-check class="w-5 h-5" />
                        </div>
                        <span class="text-sm">Lulus Seleksi</span>
                    </div>

                    <div class="flex-1 h-1 bg-green-500"></div>

                    {{-- Step 2: Bayar UKT --}}
                    <div class="flex items-center gap-2">
                        <div class="w-8 h-8 {{ $this->registration->ukt_paid_at ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-500' }} rounded-full flex items-center justify-center">
                            @if($this->registration->ukt_paid_at)
                                <x-heroicon-o-check class="w-5 h-5" />
                            @else
                                2
                            @endif
                        </div>
                        <span class="text-sm">Bayar UKT</span>
                    </div>

                    <div class="flex-1 h-1 {{ $this->registration->ukt_paid_at ? 'bg-green-500' : 'bg-gray-200' }}"></div>

                    {{-- Step 3: NIM --}}
                    <div class="flex items-center gap-2">
                        <div class="w-8 h-8 {{ $this->registration->nim ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-500' }} rounded-full flex items-center justify-center">
                            @if($this->registration->nim)
                                <x-heroicon-o-check class="w-5 h-5" />
                            @else
                                3
                            @endif
                        </div>
                        <span class="text-sm">Terima NIM</span>
                    </div>
                </div>
            </div>

            {{-- UKT Payment --}}
            <div class="bg-white dark:bg-gray-800 rounded-xl shadow p-6">
                <h3 class="font-semibold mb-4">Pembayaran UKT Semester 1</h3>

                <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-4">
                    <div>
                        <p class="text-sm text-gray-500">Total UKT</p>
                        <p class="text-2xl font-bold text-primary-600">
                            {{ $this->registration->formatted_ukt_amount }}
                        </p>
                    </div>

                    @if($this->registration->ukt_paid_at)
                        <span class="inline-flex items-center gap-2 bg-green-100 text-green-800 px-4 py-2 rounded-full">
                            <x-heroicon-o-check-circle class="w-5 h-5" />
                            Sudah Dibayar
                        </span>
                    @else
                        <button
                            wire:click="payUkt"
                            class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition"
                        >
                            Bayar UKT
                        </button>
                    @endif
                </div>

                <p class="text-sm text-gray-500">
                    Batas waktu: {{ $this->registration->admissionWave->re_registration_deadline?->format('d F Y') }}
                </p>
            </div>

            {{-- Additional Documents Form --}}
            <div class="bg-white dark:bg-gray-800 rounded-xl shadow p-6">
                <form wire:submit.prevent>
                    {{ $this->form }}
                </form>
            </div>

            {{-- Complete Button --}}
            @if($this->registration->ukt_paid_at && !$this->registration->nim)
                <div class="text-center">
                    <button
                        wire:click="completeReRegistration"
                        class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 px-8 rounded-lg transition text-lg"
                    >
                        <x-heroicon-o-academic-cap class="w-6 h-6 inline mr-2" />
                        Selesaikan Daftar Ulang & Dapatkan NIM
                    </button>
                </div>
            @endif

            {{-- NIM Card (if completed) --}}
            @if($this->registration->nim)
                <div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-2xl p-8 text-white text-center">
                    <p class="text-blue-100 mb-2">Nomor Induk Mahasiswa Anda</p>
                    <p class="text-4xl font-bold tracking-wider mb-4">{{ $this->registration->nim }}</p>
                    <p class="text-blue-100 text-sm">
                        Simpan nomor ini dengan baik. NIM akan digunakan selama masa studi Anda.
                    </p>
                </div>
            @endif

        </div>
    @endif

    {{-- Midtrans Snap JS for UKT --}}
    @if($this->snapToken)
        <script src="<https://app.sandbox.midtrans.com/snap/snap.js>" data-client-key="{{ $this->clientKey }}"></script>
        <script>
            window.snap.pay('{{ $this->snapToken }}', {
                onSuccess: function(result) {
                    window.location.href = '{{ route("applicant.ukt.finish") }}?status=success';
                },
                onPending: function(result) {
                    window.location.href = '{{ route("applicant.ukt.finish") }}?status=pending';
                },
                onError: function(result) {
                    window.location.href = '{{ route("applicant.ukt.finish") }}?status=error';
                },
                onClose: function() {}
            });
        </script>
    @endif
</x-filament-panels::page>

Route untuk UKT callback:

// routes/web.php

Route::get('/ukt/finish', function (Request $request) {
    return redirect()->route('filament.applicant.pages.re-registration')
        ->with('ukt_status', $request->get('status'));
})->name('applicant.ukt.finish')->middleware(['auth']);

Manual Review (20%):

  • ✅ Progress steps visual
  • ✅ UKT payment via Midtrans
  • ✅ NIM generation
  • ✅ Conditional navigation
💡 RECAP PENGUMUMAN & DAFTAR ULANG:

Prompts: 2
Pages created:
- Public announcement (tanpa login)
- Re-Registration (Applicant Panel)

Features:
- Verifikasi dengan tanggal lahir
- Confetti celebration untuk yang lulus
- Different UI per result (passed/failed/waitlist)
- Progress steps untuk daftar ulang
- UKT payment via Midtrans
- NIM auto-generation
- Conditional navigation

Controllers:
- AnnouncementController (public)

Yang di-review manual:
- NIM format sesuai kampus
- Deadline validation
- Waitlist handling

Di bagian terakhir, kita akan buat penutup dengan recap perjalanan, statistik vibe coding, dan rekomendasi kelas BuildWithAngga.

Bagian 10: Penutup & Rekomendasi BuildWithAngga

Selamat! Kamu sudah berhasil membangun sistem Penerimaan Mahasiswa Baru (PMB) yang lengkap dengan pendekatan vibe coding. Mari kita recap perjalanan kita dan lihat apa yang bisa dipelajari selanjutnya.

Recap Perjalanan Building Sistem PMB

SISTEM PMB ONLINE - COMPLETE!

📁 Project Structure:
├── Admin Panel (/admin)
│   ├── Dashboard (statistik real-time)
│   ├── Master Data
│   │   ├── Fakultas
│   │   ├── Program Studi
│   │   ├── Gelombang Pendaftaran
│   │   └── Jalur Pendaftaran
│   ├── Pendaftaran
│   │   ├── Data Pendaftar (view, filter)
│   │   ├── Verifikasi Dokumen
│   │   ├── Input Nilai Ujian
│   │   └── Generate Pengumuman
│   └── Laporan
│       └── Export Excel
│
├── Applicant Panel (/pendaftaran)
│   ├── Dashboard (progress tracking)
│   ├── Form Pendaftaran (5-step wizard)
│   ├── Pembayaran (Midtrans)
│   ├── Cetak Kartu Ujian (PDF)
│   └── Daftar Ulang
│
├── Public Pages
│   └── Pengumuman Kelulusan
│
├── Services
│   ├── MidtransService
│   └── ExamCardService
│
└── Database
    ├── 7 Tables
    ├── 7 Models
    └── Seeders

Statistik Vibe Coding

Berdasarkan tutorial ini, berikut breakdown antara prompt ke Claude vs manual coding:

KomponenDari PromptManual EditTotal Lines
Migrations95%5%~400
Models90%10%~600
Filament Resources85%15%~800
Custom Pages80%20%~500
Services85%15%~300
Blade Views75%25%~600
Controllers90%10%~200
Total~85%~15%~3.400

Yang paling banyak di-edit manual:

  • Validation rules spesifik Indonesia (NIK, format alamat)
  • Business logic seleksi dan ranking
  • UI/UX tweaks untuk Blade views
  • Edge cases handling

Yang hampir 100% dari prompt:

  • Database migrations
  • Eloquent relationships
  • Basic CRUD operations
  • Midtrans integration boilerplate

Apa yang Sudah Kamu Pelajari

✓ Setup Filament 4 dengan Multiple Panels
  └── Admin dan Applicant panel terpisah tapi satu codebase

✓ Database Design untuk Sistem Akademik
  └── Relasi kompleks, JSON fields, status management

✓ Multi-Step Form Wizard
  └── 5 tahap dengan validation, auto-save, file upload

✓ Payment Gateway Integration
  └── Midtrans Snap API untuk biaya daftar dan UKT

✓ PDF Generation
  └── Kartu ujian dengan foto, QR code, dan styling

✓ Public Pages dengan Laravel
  └── Pengumuman tanpa authentication

✓ Status Workflow Management
  └── 13 status dengan transisi yang jelas

✓ Vibe Coding Workflow
  └── 85% prompt, 15% manual review dan edit

Fitur yang Bisa Ditambahkan

Sistem PMB ini sudah functional, tapi masih banyak yang bisa dikembangkan:

ENHANCEMENT IDEAS:

🔔 Notification System
├── Email notification setiap status change
├── WhatsApp notification (via Fonnte/WaBlas)
└── Push notification ke mobile

📊 Advanced Analytics
├── Conversion funnel (register → submit → pay → pass)
├── Demographics analysis per wilayah
├── Trend comparison year-over-year
└── Program studi demand analysis

🔗 Integration
├── LTMPT/SNPMB API untuk sinkronisasi data
├── PDDIKTI integration untuk reporting
├── SSO dengan sistem kampus existing
└── LMS integration untuk yang diterima

📝 Advanced Features
├── Online exam/CBT module
├── Interview scheduling dengan calendar
├── Document OCR untuk auto-fill data
├── Chatbot FAQ untuk calon mahasiswa
├── Mobile app untuk monitoring

🛡️ Security & Audit
├── Complete audit trail
├── Two-factor authentication
├── Rate limiting untuk form submission
└── Data encryption untuk dokumen sensitif

Rekomendasi Kelas Gratis BuildWithAngga

Sebelum lanjut ke project yang lebih kompleks, pastikan fundamental kamu kuat. Kelas-kelas gratis ini akan membantu:

KelasApa yang Dipelajari
Laravel FundamentalMVC pattern, routing, Eloquent ORM, Blade templating — fondasi untuk semua project Laravel
PHP untuk PemulaSyntax PHP, OOP, functions, error handling — wajib sebelum Laravel
JavaScript FundamentalDOM manipulation, events, async/await — untuk interaksi frontend
Tailwind CSSUtility-first CSS, responsive design — styling modern tanpa ribet
SQL for BeginnersQuery dasar, JOIN, indexing — optimasi database
Git untuk PemulaVersion control, branching, collaboration — wajib untuk tim

Semua kelas di atas GRATIS dan bisa diakses kapan saja di BuildWithAngga.

Rekomendasi Kelas Premium BuildWithAngga

Untuk menguasai skill yang dipakai di tutorial ini secara mendalam:

KelasCocok Untuk
Full-Stack Laravel + Vue.jsBuild SPA dengan Laravel backend dan Vue frontend — lebih modern dari Blade
Laravel Filament MasteryDeep dive Filament: custom fields, actions, plugins, themes — jadi expert admin panel
Laravel API DevelopmentRESTful API, authentication, rate limiting — kalau mau bikin mobile app untuk PMB
DevOps untuk DeveloperDocker, CI/CD, monitoring — deploy sistem PMB ke production dengan proper
Build SaaS dengan LaravelMulti-tenant, subscription, billing — kalau mau jual sistem PMB ke banyak kampus
Laravel Payment IntegrationMidtrans, Xendit, Stripe deep dive — payment untuk berbagai use case

Benefit Kelas Premium BuildWithAngga

┌─────────────────────────────────────────────────────────┐
│                                                         │
│   🎯 BENEFIT KELAS PREMIUM BUILDWITHANGGA               │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   ✅ Project Portfolio-Ready                            │
│      Setiap kelas menghasilkan project yang bisa       │
│      langsung masuk portfolio untuk melamar kerja      │
│                                                         │
│   ✅ Akses Seumur Hidup                                 │
│      Sekali bayar, akses forever. Update materi        │
│      juga gratis selamanya                             │
│                                                         │
│   ✅ Konsultasi dengan Mentor                           │
│      Stuck di project? Tanya langsung ke mentor        │
│      yang berpengalaman di industri                    │
│                                                         │
│   ✅ Sertifikat Resmi                                   │
│      Sertifikat completion yang bisa di-share          │
│      di LinkedIn dan CV                                │
│                                                         │
│   ✅ Komunitas 900.000+ Students                        │
│      Network dengan sesama developer Indonesia,        │
│      sharing job info, dan collaboration               │
│                                                         │
│   ✅ Always Up-to-Date                                  │
│      Materi selalu diupdate mengikuti versi            │
│      terbaru framework dan best practices              │
│                                                         │
└─────────────────────────────────────────────────────────┘

Tips Melanjutkan Project Ini

Kalau kamu mau develop sistem PMB ini lebih lanjut untuk kampus nyata:

  1. Diskusi dengan Stakeholder
    • Interview panitia PMB yang ada
    • Pahami SOP dan regulasi kampus
    • Identifikasi pain points spesifik
  2. Customize Sesuai Kebutuhan
    • Sesuaikan field dengan form resmi kampus
    • Tambahkan jalur khusus (afirmasi, atlet, dll)
    • Integrasi dengan sistem existing
  3. Testing Menyeluruh
    • Unit test untuk business logic
    • Feature test untuk flow utama
    • UAT dengan sample users
  4. Deployment Proper
    • Server dengan spek yang cukup
    • SSL certificate
    • Backup otomatis
    • Monitoring dan alerting
  5. Dokumentasi
    • User manual untuk admin
    • FAQ untuk calon mahasiswa
    • Technical docs untuk maintenance

Closing Statement

Sistem PMB yang kita bangun di tutorial ini adalah contoh bagaimana vibe coding bisa mempercepat development tanpa mengorbankan kualitas. Dengan 85% kode dari prompt dan 15% review manual, kita berhasil build sistem yang:

  • Complete dari pendaftaran sampai daftar ulang
  • User-friendly untuk admin dan pendaftar
  • Terintegrasi dengan payment gateway
  • Generate dokumen PDF
  • Responsive dan modern

Ini bukan magic — ini adalah skill yang bisa dipelajari. Semakin sering kamu practice vibe coding, semakin bagus prompt yang kamu tulis, dan semakin efisien workflow-mu.

Yang penting diingat: AI adalah partner, bukan pengganti. Kamu tetap perlu understand kode yang di-generate, review untuk security dan edge cases, dan own hasilnya.

Selamat coding, dan sampai ketemu di tutorial selanjutnya!