đŸ’ģ Pemrograman Web 2
🎓 Pertemuan
Pertemuan 13: CRUD Anggota Dengan Laravel

MODUL PEMROGRAMAN WEBSITE 2

Mata Kuliah: Pemrograman Website 2
Kode MK: INF2419
SKS: 3 (Praktikum)
Semester: Genap 2025/2026
Program Studi: Informatika
Fakultas: FEBI / Saintek
Universitas: UIN K.H. Abdurrahman Wahid Pekalongan

Dosen Pengampu: Mohammad Reza Maulana, M.Kom
NIP: 199110082025051002

Pertemuan: 13 dari 16
Durasi: 150 menit (3 × 50 menit)
Studi Kasus Berkelanjutan: Sistem Manajemen Perpustakaan


PERTEMUAN 13

CRUD ANGGOTA DENGAN LARAVEL

A. INFORMASI PERTEMUAN

AspekKeterangan
Capaian Pembelajaran Lulusan (CPL)CPL06: Mempunyai pengetahuan dalam mengembangkan algoritma/metode yang diimplementasikan dalam perangkat lunak.
Capaian Pembelajaran Mata Kuliah (CPMK)CPMK06.1: Mampu mengimplementasikan backend web menggunakan PHP dan Laravel yang terintegrasi dengan database.
Sub-CPMKSub-CPMK06.1.2: Mengimplementasikan CRUD menggunakan Laravel dan ORM (Eloquent).
Indikator PencapaianMahasiswa mampu:
1. Menerapkan pola CRUD yang konsisten
2. Implementasi DRY (Don't Repeat Yourself) principle
3. Membuat form untuk data anggota dengan validasi advanced
4. Handle input tanggal dengan date picker
5. Custom validation untuk data personal
6. Refactoring code untuk reusability
7. Mengorganisir code dengan baik
8. Menyiapkan relasi antar tabel
Alokasi Waktuâ€ĸ Teori: 60 menit
â€ĸ Praktikum: 90 menit
â€ĸ Total: 150 menit (3 × 50 menit)

B. PENDAHULUAN

1. Deskripsi Singkat

Pertemuan ketigabelas ini fokus pada implementasi CRUD untuk data anggota perpustakaan dengan mereplikasi pola yang telah dipelajari di pertemuan 12 (CRUD Buku). Mahasiswa akan belajar menerapkan DRY principle, refactoring code, dan membuat validasi yang lebih advanced untuk data personal seperti email, telepon, dan tanggal lahir. Pertemuan ini juga menyiapkan fondasi untuk relasi antar tabel di pertemuan berikutnya.

2. Keterkaitan dengan Pertemuan Lain

Pertemuan ini melanjutkan pembelajaran CRUD operations:

  • Pertemuan 10: Model & Migration Anggota sudah dibuat
  • Pertemuan 11: Pola Controller & Blade View dipelajari melalui BukuController — pola yang sama akan direplikasi untuk Anggota di pertemuan ini
  • Pertemuan 12: Pola CRUD Buku (Create/Update/Delete) sebagai template yang diikuti
  • Pertemuan 13: [SEKARANG] CRUD Anggota lengkap — mulai dari Read (index & show) hingga Create, Update, Delete
  • Pertemuan 14: Relasi Anggota-Transaksi untuk peminjaman
  • Pertemuan 15: Integrasi sistem lengkap

Konsistensi Pattern:

CRUD Buku (P12)        →  CRUD Anggota (P13)
├─ Form Create/Edit    →  ├─ Form Create/Edit (+ date picker)
├─ Validation Rules    →  ├─ Validation Advanced (email, phone)
├─ Store/Update        →  ├─ Store/Update (same pattern)
├─ Delete              →  ├─ Delete (same pattern)
└─ Flash Messages      →  └─ Flash Messages (same pattern)

3. Manfaat Pembelajaran

  1. Mahir menerapkan pola CRUD secara konsisten
  2. Memahami dan menerapkan DRY principle
  3. Dapat membuat validasi untuk berbagai tipe data
  4. Mampu handle input tanggal dengan baik
  5. Terampil refactoring code untuk maintainability
  6. Siap mengembangkan modul CRUD lainnya

4. Relevansi dengan Studi Kasus

Dalam sistem perpustakaan, CRUD Anggota digunakan untuk:

  • Create: Mendaftarkan anggota baru perpustakaan
  • Read: Melihat daftar dan detail anggota (sudah di pertemuan 11)
  • Update: Mengubah data anggota (alamat, telepon, status)
  • Delete: Menonaktifkan atau menghapus anggota yang tidak aktif

C. MATERI TEORI

1. DRY Principle (Don't Repeat Yourself)

a. Apa itu DRY Principle?

DRY (Don't Repeat Yourself) adalah prinsip pemrograman yang menekankan untuk menghindari duplikasi code. Setiap logic harus memiliki single, unambiguous representation dalam sistem.

Quote Original (Andy Hunt & Dave Thomas):

"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."

b. Mengapa DRY Penting?

Tanpa DRY:

// BukuController
public function store(Request $request) {
    $request->validate([
        'judul' => 'required|max:200',
        'harga' => 'required|numeric',
    ]);
    Buku::create($request->all());
    return redirect()->route('buku.index')
                     ->with('success', 'Data berhasil disimpan');
}
 
// AnggotaController
public function store(Request $request) {
    $request->validate([
        'nama' => 'required|max:100',
        'email' => 'required|email',
    ]);
    Anggota::create($request->all());
    return redirect()->route('anggota.index')
                     ->with('success', 'Data berhasil disimpan');
}
 
// Masalah:
// 1. Pattern yang sama diulang-ulang
// 2. Jika ada perubahan, harus update di banyak tempat
// 3. Risk of inconsistency

Dengan DRY:

// Gunakan Form Request (single source of truth untuk validation)
class StoreBukuRequest extends FormRequest {
    public function rules() {
        return [ /* validation rules */ ];
    }
}
 
// Controller jadi sangat simple
public function store(StoreBukuRequest $request) {
    Buku::create($request->validated());
    return redirect()->route('buku.index')
                     ->with('success', 'Data berhasil disimpan');
}

c. Penerapan DRY di Laravel

1. Form Request untuk Validation:

// ❌ WRONG: Validasi di controller (repetitive)
public function store(Request $request) {
    $request->validate([...]);
}
 
public function update(Request $request) {
    $request->validate([...]); // Sama dengan store!
}
 
// ✅ CORRECT: Gunakan Form Request
public function store(StoreBukuRequest $request) { }
public function update(UpdateBukuRequest $request) { }

2. Eloquent Accessors & Mutators:

// ❌ WRONG: Format harga di semua view
@foreach($bukus as $buku)
    Rp {{ number_format($buku->harga, 0, ',', '.') }}
@endforeach
 
// ✅ CORRECT: Accessor di Model (single source)
// Model
public function getHargaFormatAttribute() {
    return 'Rp ' . number_format($this->harga, 0, ',', '.');
}
 
// View
{{ $buku->harga_format }}

3. Blade Components:

// ❌ WRONG: Duplikasi form elements
<div class="mb-3">
    <label>Judul</label>
    <input type="text" name="judul" class="form-control">
</div>
 
<div class="mb-3">
    <label>Kategori</label>
    <input type="text" name="kategori" class="form-control">
</div>
 
// ✅ CORRECT: Blade Component
<x-input name="judul" label="Judul" />
<x-input name="kategori" label="Kategori" />

4. Traits untuk Reusable Logic:

trait HasCreatedBy {
    public static function bootHasCreatedBy() {
        static::creating(function ($model) {
            $model->created_by = auth()->id();
        });
    }
}
 
// Gunakan di multiple models
class Buku extends Model {
    use HasCreatedBy;
}
 
class Anggota extends Model {
    use HasCreatedBy;
}

d. WET vs DRY

WET (Write Everything Twice / We Enjoy Typing):

  • Copy-paste code
  • Duplikasi logic
  • Hard to maintain
  • Inconsistent behavior

DRY (Don't Repeat Yourself):

  • Abstraction & reusability
  • Single source of truth
  • Easy to maintain
  • Consistent behavior

2. Code Organization Best Practices

a. Folder Structure

Laravel Standard Structure:

app/
├── Http/
│   ├── Controllers/
│   │   ├── BukuController.php
│   │   ├── AnggotaController.php
│   │   └── TransaksiController.php
│   ├── Requests/
│   │   ├── StoreBukuRequest.php
│   │   ├── UpdateBukuRequest.php
│   │   ├── StoreAnggotaRequest.php
│   │   └── UpdateAnggotaRequest.php
│   └── Middleware/
├── Models/
│   ├── Buku.php
│   ├── Anggota.php
│   └── Transaksi.php
└── Services/ (optional - for business logic)
    └── TransaksiService.php

resources/
├── views/
│   ├── layouts/
│   │   ├── app.blade.php
│   │   ├── navbar.blade.php
│   │   └── footer.blade.php
│   ├── components/
│   │   ├── alert.blade.php
│   │   └── input.blade.php
│   ├── buku/
│   │   ├── index.blade.php
│   │   ├── create.blade.php
│   │   ├── edit.blade.php
│   │   └── show.blade.php
│   └── anggota/
│       ├── index.blade.php
│       ├── create.blade.php
│       ├── edit.blade.php
│       └── show.blade.php

b. Naming Conventions

Controllers:

  • Singular, PascalCase
  • Suffix: Controller
  • Example: BukuController, AnggotaController

Models:

  • Singular, PascalCase
  • No suffix
  • Example: Buku, Anggota, Transaksi

Form Requests:

  • Action + Model name
  • Example: StoreBukuRequest, UpdateAnggotaRequest

Views:

  • Plural folder, lowercase
  • File: action name
  • Example: buku/create.blade.php, anggota/edit.blade.php

Routes:

  • Lowercase, plural
  • Example: /buku, /anggota, /transaksi

c. Controller Organization

Thin Controller Pattern:

// ✅ GOOD: Thin controller
class AnggotaController extends Controller
{
    public function store(StoreAnggotaRequest $request)
    {
        Anggota::create($request->validated());
        
        return redirect()->route('anggota.index')
                         ->with('success', 'Anggota berhasil ditambahkan!');
    }
}
 
// ❌ BAD: Fat controller
class AnggotaController extends Controller
{
    public function store(Request $request)
    {
        // Validation logic (should be in FormRequest)
        if (empty($request->nama)) {
            return back()->with('error', 'Nama wajib diisi');
        }
        
        // Business logic (should be in Service/Model)
        $kode = 'AGT-' . str_pad(Anggota::count() + 1, 3, '0', STR_PAD_LEFT);
        
        // Formatting logic (should be in Model)
        $telepon = str_replace(['-', ' '], '', $request->telepon);
        
        // Database operation
        Anggota::create([
            'kode_anggota' => $kode,
            'nama' => $request->nama,
            'telepon' => $telepon,
            // ... many more fields
        ]);
        
        return redirect()->route('anggota.index')
                         ->with('success', 'Anggota berhasil ditambahkan!');
    }
}

3. Advanced Form Validation

a. Validation untuk Data Personal

Email Validation:

'email' => [
    'required',
    'email',                        // Format email valid
    'unique:anggota,email',         // Unik di tabel anggota
    'max:100',
]

Phone Number Validation:

'telepon' => [
    'required',
    'regex:/^(\+62|62|0)[0-9]{9,12}$/', // Format Indonesia
    'min:10',
    'max:15',
]
 
// Penjelasan regex:
// ^(\+62|62|0)  : Diawali +62, 62, atau 0
// [0-9]{9,12}   : Diikuti 9-12 digit angka
// $             : End of string

Date Validation:

'tanggal_lahir' => [
    'required',
    'date',                         // Format tanggal valid
    'before:today',                 // Sebelum hari ini
    'after:1900-01-01',            // Setelah tahun 1900
]
 
'tanggal_daftar' => [
    'required',
    'date',
    'before_or_equal:today',       // Hari ini atau sebelumnya
]

Conditional Validation:

public function rules()
{
    $rules = [
        'nama' => 'required|max:100',
        'email' => 'required|email|unique:anggota,email',
    ];
    
    // Tambah validation untuk update (ignore current record)
    if ($this->method() == 'PUT') {
        $rules['email'] = 'required|email|unique:anggota,email,' . $this->anggota->id;
    }
    
    // Conditional: jika pekerjaan = "Mahasiswa", NIM required
    if ($this->pekerjaan == 'Mahasiswa') {
        $rules['nim'] = 'required|string|max:20';
    }
    
    return $rules;
}

b. Custom Validation Rules

Method 1: Inline Rule Object

use Illuminate\Validation\Rule;
 
'status' => [
    'required',
    Rule::in(['Aktif', 'Nonaktif']),
]

Method 2: Custom Rule Class

php artisan make:rule MinimalUsia
<?php
 
namespace App\Rules;
 
use Illuminate\Contracts\Validation\Rule;
use Carbon\Carbon;
 
class MinimalUsia implements Rule
{
    private $minimalUsia;
    
    public function __construct($minimalUsia = 17)
    {
        $this->minimalUsia = $minimalUsia;
    }
    
    public function passes($attribute, $value)
    {
        $umur = Carbon::parse($value)->age;
        return $umur >= $this->minimalUsia;
    }
    
    public function message()
    {
        return 'Umur minimal harus ' . $this->minimalUsia . ' tahun.';
    }
}

Usage:

use App\Rules\MinimalUsia;
 
'tanggal_lahir' => [
    'required',
    'date',
    new MinimalUsia(17), // Minimal 17 tahun
]

Method 3: Closure Validation

'telepon' => [
    'required',
    function ($attribute, $value, $fail) {
        if (!preg_match('/^08[0-9]{8,11}$/', $value)) {
            $fail('Format nomor telepon tidak valid. Harus diawali 08.');
        }
    },
]

c. Validation Messages Indonesia

public function messages()
{
    return [
        'nama.required' => 'Nama anggota wajib diisi.',
        'email.required' => 'Email wajib diisi.',
        'email.email' => 'Format email tidak valid.',
        'email.unique' => 'Email sudah terdaftar.',
        'telepon.required' => 'Nomor telepon wajib diisi.',
        'telepon.regex' => 'Format nomor telepon tidak valid. Contoh: 081234567890',
        'tanggal_lahir.required' => 'Tanggal lahir wajib diisi.',
        'tanggal_lahir.date' => 'Format tanggal tidak valid.',
        'tanggal_lahir.before' => 'Tanggal lahir harus sebelum hari ini.',
        'alamat.required' => 'Alamat wajib diisi.',
        'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
        'jenis_kelamin.in' => 'Jenis kelamin tidak valid.',
    ];
}
 
public function attributes()
{
    return [
        'tanggal_lahir' => 'tanggal lahir',
        'tanggal_daftar' => 'tanggal pendaftaran',
        'jenis_kelamin' => 'jenis kelamin',
    ];
}

4. Handle Date Input di Laravel

a. Date Picker Implementation

Frontend (Bootstrap Datepicker atau HTML5):

Option 1: HTML5 Date Input (Simple)

<input type="date" 
       name="tanggal_lahir" 
       class="form-control"
       max="{{ date('Y-m-d') }}"
       value="{{ old('tanggal_lahir', $anggota->tanggal_lahir?->format('Y-m-d')) }}">

Option 2: Flatpickr (Better UX)

{{-- Include Flatpickr CSS --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
 
<input type="text" 
       name="tanggal_lahir" 
       id="tanggal_lahir"
       class="form-control"
       value="{{ old('tanggal_lahir', $anggota->tanggal_lahir?->format('Y-m-d')) }}">
 
{{-- Include Flatpickr JS --}}
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script>
    flatpickr("#tanggal_lahir", {
        dateFormat: "Y-m-d",
        maxDate: "today",
        locale: "id",
    });
</script>

b. Date Casting di Model

class Anggota extends Model
{
    protected $casts = [
        'tanggal_lahir' => 'date',
        'tanggal_daftar' => 'date',
    ];
    
    // Accessor untuk umur
    public function getUmurAttribute()
    {
        return $this->tanggal_lahir?->age;
    }
    
    // Accessor untuk lama menjadi anggota (hari)
    public function getLamaAnggotaAttribute()
    {
        return $this->tanggal_daftar?->diffInDays(now());
    }
}

Usage di View:

{{-- Format date --}}
{{ $anggota->tanggal_lahir->format('d F Y') }}
 
{{-- Accessor --}}
Umur: {{ $anggota->umur }} tahun
Lama anggota: {{ $anggota->lama_anggota }} hari

c. Date Validation

'tanggal_lahir' => [
    'required',
    'date',
    'before:today',                    // Harus sebelum hari ini
    'after:1900-01-01',               // Harus setelah 1900
],
 
'tanggal_daftar' => [
    'required',
    'date',
    'before_or_equal:today',          // Hari ini atau sebelumnya
    'after_or_equal:' . now()->subYears(10)->format('Y-m-d'), // Max 10 tahun lalu
],

5. Perbedaan Validation Buku vs Anggota

AspekBukuAnggota
Primary IdentifierKode Buku (string)Kode Anggota (string)
Unique FieldsISBN (nullable)Email (required)
Numeric FieldsHarga, Stok, Tahun-
Date Fields-Tanggal Lahir, Tanggal Daftar
Enum FieldsKategori (5 options)Jenis Kelamin (2 options), Status (2 options)
Text FieldsJudul, Pengarang, PenerbitNama, Alamat, Pekerjaan
Special ValidationYear range, Price min:0Email format, Phone regex, Age validation

D. PRAKTIKUM

1. Tujuan Praktikum

  1. Membuat Form Request untuk Anggota
  2. Implementasi form create dan edit anggota
  3. Handle date input dengan date picker
  4. Validation advanced untuk data personal
  5. Store, update, dan delete anggota
  6. Konsistensi pattern dengan CRUD Buku

2. PRAKTIKUM 1: Form Request Anggota

Tujuan

Membuat Form Request untuk validasi data anggota.

Langkah-langkah

a. Generate Form Request

php artisan make:request StoreAnggotaRequest
php artisan make:request UpdateAnggotaRequest

b. Edit StoreAnggotaRequest

File: app/Http/Requests/StoreAnggotaRequest.php

<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class StoreAnggotaRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'kode_anggota' => 'required|string|max:20|unique:anggota,kode_anggota',
            'nama' => 'required|string|max:100',
            'email' => 'required|email|unique:anggota,email|max:100',
            'telepon' => [
                'required',
                'regex:/^(\+62|62|0)[0-9]{9,12}$/',
                'min:10',
                'max:15',
            ],
            'alamat' => 'required|string',
            'tanggal_lahir' => [
                'required',
                'date',
                'before:today',
                'after:1900-01-01',
            ],
            'jenis_kelamin' => 'required|in:Laki-laki,Perempuan',
            'pekerjaan' => 'nullable|string|max:50',
            'tanggal_daftar' => [
                'required',
                'date',
                'before_or_equal:today',
            ],
            'status' => 'required|in:Aktif,Nonaktif',
        ];
    }
 
    /**
     * Get custom error messages.
     */
    public function messages(): array
    {
        return [
            'kode_anggota.required' => 'Kode anggota wajib diisi.',
            'kode_anggota.unique' => 'Kode anggota sudah digunakan.',
            'kode_anggota.max' => 'Kode anggota maksimal 20 karakter.',
            
            'nama.required' => 'Nama anggota wajib diisi.',
            'nama.max' => 'Nama maksimal 100 karakter.',
            
            'email.required' => 'Email wajib diisi.',
            'email.email' => 'Format email tidak valid.',
            'email.unique' => 'Email sudah terdaftar.',
            'email.max' => 'Email maksimal 100 karakter.',
            
            'telepon.required' => 'Nomor telepon wajib diisi.',
            'telepon.regex' => 'Format nomor telepon tidak valid. Contoh: 081234567890 atau +6281234567890',
            'telepon.min' => 'Nomor telepon minimal 10 karakter.',
            'telepon.max' => 'Nomor telepon maksimal 15 karakter.',
            
            'alamat.required' => 'Alamat wajib diisi.',
            
            'tanggal_lahir.required' => 'Tanggal lahir wajib diisi.',
            'tanggal_lahir.date' => 'Format tanggal lahir tidak valid.',
            'tanggal_lahir.before' => 'Tanggal lahir harus sebelum hari ini.',
            'tanggal_lahir.after' => 'Tanggal lahir tidak valid.',
            
            'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
            'jenis_kelamin.in' => 'Jenis kelamin tidak valid.',
            
            'pekerjaan.max' => 'Pekerjaan maksimal 50 karakter.',
            
            'tanggal_daftar.required' => 'Tanggal pendaftaran wajib diisi.',
            'tanggal_daftar.date' => 'Format tanggal pendaftaran tidak valid.',
            'tanggal_daftar.before_or_equal' => 'Tanggal pendaftaran tidak boleh di masa depan.',
            
            'status.required' => 'Status wajib dipilih.',
            'status.in' => 'Status tidak valid.',
        ];
    }
 
    /**
     * Get custom attribute names.
     */
    public function attributes(): array
    {
        return [
            'kode_anggota' => 'kode anggota',
            'nama' => 'nama',
            'email' => 'email',
            'telepon' => 'nomor telepon',
            'alamat' => 'alamat',
            'tanggal_lahir' => 'tanggal lahir',
            'jenis_kelamin' => 'jenis kelamin',
            'pekerjaan' => 'pekerjaan',
            'tanggal_daftar' => 'tanggal pendaftaran',
            'status' => 'status',
        ];
    }
}

c. Edit UpdateAnggotaRequest

File: app/Http/Requests/UpdateAnggotaRequest.php

<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class UpdateAnggotaRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        // Get anggota ID from route parameter
        $anggotaId = $this->route('anggota');
        
        return [
            'kode_anggota' => 'required|string|max:20|unique:anggota,kode_anggota,' . $anggotaId,
            'nama' => 'required|string|max:100',
            'email' => 'required|email|unique:anggota,email,' . $anggotaId . '|max:100',
            'telepon' => [
                'required',
                'regex:/^(\+62|62|0)[0-9]{9,12}$/',
                'min:10',
                'max:15',
            ],
            'alamat' => 'required|string',
            'tanggal_lahir' => [
                'required',
                'date',
                'before:today',
                'after:1900-01-01',
            ],
            'jenis_kelamin' => 'required|in:Laki-laki,Perempuan',
            'pekerjaan' => 'nullable|string|max:50',
            'tanggal_daftar' => [
                'required',
                'date',
                'before_or_equal:today',
            ],
            'status' => 'required|in:Aktif,Nonaktif',
        ];
    }
 
    /**
     * Get custom error messages.
     */
    public function messages(): array
    {
        return [
            'kode_anggota.unique' => 'Kode anggota sudah digunakan.',
            'email.unique' => 'Email sudah terdaftar.',
            'telepon.regex' => 'Format nomor telepon tidak valid. Contoh: 081234567890',
            'tanggal_lahir.before' => 'Tanggal lahir harus sebelum hari ini.',
            'tanggal_daftar.before_or_equal' => 'Tanggal pendaftaran tidak boleh di masa depan.',
        ];
    }
}

3. PRAKTIKUM 2: Form Create Anggota

Tujuan

Membuat form untuk mendaftarkan anggota baru dengan date picker.

Langkah-langkah

a. Update AnggotaController - Method create()

File: app/Http/Controllers/AnggotaController.php

/**
 * Show the form for creating a new resource.
 */
public function create()
{
    return view('anggota.create');
}

b. Buat View Create

File: resources/views/anggota/create.blade.php

@extends('layouts.app')
 
@section('title', 'Tambah Anggota')
 
@push('styles')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
@endpush
 
@section('content')
<div class="row justify-content-center">
    <div class="col-md-10">
        <div class="card">
            <div class="card-header bg-success text-white">
                <h4 class="mb-0">
                    <i class="bi bi-person-plus"></i>
                    Tambah Anggota Baru
                </h4>
            </div>
            <div class="card-body">
                <form action="{{ route('anggota.store') }}" method="POST">
                    @csrf
                    
                    <div class="row">
                        {{-- Kode Anggota --}}
                        <div class="col-md-4 mb-3">
                            <label for="kode_anggota" class="form-label">
                                Kode Anggota <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="kode_anggota" 
                                   id="kode_anggota" 
                                   class="form-control @error('kode_anggota') is-invalid @enderror"
                                   value="{{ old('kode_anggota') }}"
                                   placeholder="Contoh: AGT-001">
                            @error('kode_anggota')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                            <small class="text-muted">Format: AGT-XXX</small>
                        </div>
                        
                        {{-- Nama --}}
                        <div class="col-md-8 mb-3">
                            <label for="nama" class="form-label">
                                Nama Lengkap <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="nama" 
                                   id="nama" 
                                   class="form-control @error('nama') is-invalid @enderror"
                                   value="{{ old('nama') }}"
                                   placeholder="Nama lengkap anggota">
                            @error('nama')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Email --}}
                        <div class="col-md-6 mb-3">
                            <label for="email" class="form-label">
                                Email <span class="text-danger">*</span>
                            </label>
                            <input type="email" 
                                   name="email" 
                                   id="email" 
                                   class="form-control @error('email') is-invalid @enderror"
                                   value="{{ old('email') }}"
                                   placeholder="email@example.com">
                            @error('email')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Telepon --}}
                        <div class="col-md-6 mb-3">
                            <label for="telepon" class="form-label">
                                Nomor Telepon <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="telepon" 
                                   id="telepon" 
                                   class="form-control @error('telepon') is-invalid @enderror"
                                   value="{{ old('telepon') }}"
                                   placeholder="081234567890">
                            @error('telepon')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                            <small class="text-muted">Format: 08xxxxxxxxxx atau +628xxxxxxxxxx</small>
                        </div>
                    </div>
                    
                    {{-- Alamat --}}
                    <div class="mb-3">
                        <label for="alamat" class="form-label">
                            Alamat Lengkap <span class="text-danger">*</span>
                        </label>
                        <textarea name="alamat" 
                                  id="alamat" 
                                  rows="3" 
                                  class="form-control @error('alamat') is-invalid @enderror"
                                  placeholder="Alamat lengkap dengan kota dan kode pos">{{ old('alamat') }}</textarea>
                        @error('alamat')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <div class="row">
                        {{-- Tanggal Lahir --}}
                        <div class="col-md-4 mb-3">
                            <label for="tanggal_lahir" class="form-label">
                                Tanggal Lahir <span class="text-danger">*</span>
                            </label>
                            <input type="date" 
                                   name="tanggal_lahir" 
                                   id="tanggal_lahir" 
                                   class="form-control @error('tanggal_lahir') is-invalid @enderror"
                                   value="{{ old('tanggal_lahir') }}"
                                   max="{{ date('Y-m-d') }}">
                            @error('tanggal_lahir')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Jenis Kelamin --}}
                        <div class="col-md-4 mb-3">
                            <label for="jenis_kelamin" class="form-label">
                                Jenis Kelamin <span class="text-danger">*</span>
                            </label>
                            <select name="jenis_kelamin" 
                                    id="jenis_kelamin" 
                                    class="form-select @error('jenis_kelamin') is-invalid @enderror">
                                <option value="">-- Pilih Jenis Kelamin --</option>
                                <option value="Laki-laki" {{ old('jenis_kelamin') == 'Laki-laki' ? 'selected' : '' }}>
                                    Laki-laki
                                </option>
                                <option value="Perempuan" {{ old('jenis_kelamin') == 'Perempuan' ? 'selected' : '' }}>
                                    Perempuan
                                </option>
                            </select>
                            @error('jenis_kelamin')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Pekerjaan --}}
                        <div class="col-md-4 mb-3">
                            <label for="pekerjaan" class="form-label">Pekerjaan</label>
                            <input type="text" 
                                   name="pekerjaan" 
                                   id="pekerjaan" 
                                   class="form-control @error('pekerjaan') is-invalid @enderror"
                                   value="{{ old('pekerjaan') }}"
                                   placeholder="Contoh: Mahasiswa, Pegawai, dll">
                            @error('pekerjaan')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Tanggal Daftar --}}
                        <div class="col-md-6 mb-3">
                            <label for="tanggal_daftar" class="form-label">
                                Tanggal Pendaftaran <span class="text-danger">*</span>
                            </label>
                            <input type="date" 
                                   name="tanggal_daftar" 
                                   id="tanggal_daftar" 
                                   class="form-control @error('tanggal_daftar') is-invalid @enderror"
                                   value="{{ old('tanggal_daftar', date('Y-m-d')) }}"
                                   max="{{ date('Y-m-d') }}">
                            @error('tanggal_daftar')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Status --}}
                        <div class="col-md-6 mb-3">
                            <label for="status" class="form-label">
                                Status <span class="text-danger">*</span>
                            </label>
                            <select name="status" 
                                    id="status" 
                                    class="form-select @error('status') is-invalid @enderror">
                                <option value="Aktif" {{ old('status', 'Aktif') == 'Aktif' ? 'selected' : '' }}>
                                    Aktif
                                </option>
                                <option value="Nonaktif" {{ old('status') == 'Nonaktif' ? 'selected' : '' }}>
                                    Nonaktif
                                </option>
                            </select>
                            @error('status')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <hr>
                    
                    {{-- Buttons --}}
                    <div class="d-flex justify-content-between">
                        <a href="{{ route('anggota.index') }}" class="btn btn-secondary">
                            <i class="bi bi-arrow-left"></i> Kembali
                        </a>
                        <button type="submit" class="btn btn-success">
                            <i class="bi bi-save"></i> Simpan Anggota
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
@endsection
 
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/id.js"></script>
<script>
    // Initialize Flatpickr untuk tanggal lahir
    flatpickr("#tanggal_lahir", {
        dateFormat: "Y-m-d",
        maxDate: "today",
        locale: "id",
        altInput: true,
        altFormat: "d F Y",
    });
    
    // Initialize Flatpickr untuk tanggal daftar
    flatpickr("#tanggal_daftar", {
        dateFormat: "Y-m-d",
        maxDate: "today",
        locale: "id",
        altInput: true,
        altFormat: "d F Y",
        defaultDate: "today",
    });
    
    // Auto format telepon (hapus karakter non-digit)
    document.getElementById('telepon').addEventListener('input', function() {
        let value = this.value.replace(/[^\d+]/g, '');
        this.value = value;
    });
</script>
@endpush

c. Test Form Create

  1. Akses: http://localhost:8000/anggota/create
  2. Verifikasi:
    • Form muncul dengan semua field
    • Date picker Flatpickr berfungsi (UI lebih baik dari HTML5 date)
    • Placeholder text ada
    • Default value tanggal daftar = hari ini

4. PRAKTIKUM 3: Store Method Anggota

Tujuan

Implementasi method untuk menyimpan data anggota ke database.

Langkah-langkah

a. Update AnggotaController - Method store()

File: app/Http/Controllers/AnggotaController.php

use App\Http\Requests\StoreAnggotaRequest;
 
/**
 * Store a newly created resource in storage.
 */
public function store(StoreAnggotaRequest $request)
{
    try {
        // Create anggota baru dengan validated data
        Anggota::create($request->validated());
        
        // Redirect dengan success message
        return redirect()->route('anggota.index')
                         ->with('success', 'Anggota berhasil ditambahkan!');
                         
    } catch (\Exception $e) {
        // Redirect dengan error message jika gagal
        return redirect()->back()
                         ->withInput()
                         ->with('error', 'Gagal menambahkan anggota: ' . $e->getMessage());
    }
}

b. Test Store Operation

  1. Buka form create: http://localhost:8000/anggota/create

  2. Test 1 - Submit form kosong:

    • Klik "Simpan Anggota"
    • Verifikasi: Error validation muncul untuk required fields
  3. Test 2 - Submit dengan data valid:

    • Isi semua required fields:
      • Kode Anggota: AGT-TEST-001
      • Nama: John Doe Testing
      • Email: john.test@example.com
      • Telepon: 081234567890
      • Alamat: Jl. Testing No. 1, Jakarta
      • Tanggal Lahir: 1995-05-15
      • Jenis Kelamin: Laki-laki
      • Pekerjaan: Mahasiswa
      • Tanggal Daftar: hari ini
      • Status: Aktif
    • Klik "Simpan Anggota"
    • Verifikasi:
      • Redirect ke halaman index
      • Flash message "Anggota berhasil ditambahkan!" muncul
      • Anggota baru muncul di list
  4. Test 3 - Validation errors:

    • Email: bukan-email (test format email)
    • Telepon: 123 (test regex format)
    • Tanggal Lahir: tanggal masa depan (test before:today)
    • Klik submit
    • Verifikasi: Error validation muncul sesuai rules
  5. Test 4 - Unique validation:

    • Gunakan email yang sama dengan sebelumnya
    • Klik submit
    • Verifikasi: Error "Email sudah terdaftar."

5. PRAKTIKUM 4: Form Edit & Update Anggota

Tujuan

Membuat form edit dan implementasi update data anggota.

Langkah-langkah

a. Update AnggotaController - Method edit()

/**
 * Show the form for editing the specified resource.
 */
public function edit(string $id)
{
    $anggota = Anggota::findOrFail($id);
    return view('anggota.edit', compact('anggota'));
}

b. Buat View Edit

File: resources/views/anggota/edit.blade.php

@extends('layouts.app')
 
@section('title', 'Edit Anggota')
 
@push('styles')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
@endpush
 
@section('content')
<div class="row justify-content-center">
    <div class="col-md-10">
        <div class="card">
            <div class="card-header bg-warning">
                <h4 class="mb-0">
                    <i class="bi bi-pencil-square"></i>
                    Edit Anggota: {{ $anggota->nama }}
                </h4>
            </div>
            <div class="card-body">
                <form action="{{ route('anggota.update', $anggota->id) }}" method="POST">
                    @csrf
                    @method('PUT')
                    
                    <div class="row">
                        {{-- Kode Anggota --}}
                        <div class="col-md-4 mb-3">
                            <label for="kode_anggota" class="form-label">
                                Kode Anggota <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="kode_anggota" 
                                   id="kode_anggota" 
                                   class="form-control @error('kode_anggota') is-invalid @enderror"
                                   value="{{ old('kode_anggota', $anggota->kode_anggota) }}">
                            @error('kode_anggota')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Nama --}}
                        <div class="col-md-8 mb-3">
                            <label for="nama" class="form-label">
                                Nama Lengkap <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="nama" 
                                   id="nama" 
                                   class="form-control @error('nama') is-invalid @enderror"
                                   value="{{ old('nama', $anggota->nama) }}">
                            @error('nama')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Email --}}
                        <div class="col-md-6 mb-3">
                            <label for="email" class="form-label">
                                Email <span class="text-danger">*</span>
                            </label>
                            <input type="email" 
                                   name="email" 
                                   id="email" 
                                   class="form-control @error('email') is-invalid @enderror"
                                   value="{{ old('email', $anggota->email) }}">
                            @error('email')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Telepon --}}
                        <div class="col-md-6 mb-3">
                            <label for="telepon" class="form-label">
                                Nomor Telepon <span class="text-danger">*</span>
                            </label>
                            <input type="text" 
                                   name="telepon" 
                                   id="telepon" 
                                   class="form-control @error('telepon') is-invalid @enderror"
                                   value="{{ old('telepon', $anggota->telepon) }}">
                            @error('telepon')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    {{-- Alamat --}}
                    <div class="mb-3">
                        <label for="alamat" class="form-label">
                            Alamat Lengkap <span class="text-danger">*</span>
                        </label>
                        <textarea name="alamat" 
                                  id="alamat" 
                                  rows="3" 
                                  class="form-control @error('alamat') is-invalid @enderror">{{ old('alamat', $anggota->alamat) }}</textarea>
                        @error('alamat')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <div class="row">
                        {{-- Tanggal Lahir --}}
                        <div class="col-md-4 mb-3">
                            <label for="tanggal_lahir" class="form-label">
                                Tanggal Lahir <span class="text-danger">*</span>
                            </label>
                            <input type="date" 
                                   name="tanggal_lahir" 
                                   id="tanggal_lahir" 
                                   class="form-control @error('tanggal_lahir') is-invalid @enderror"
                                   value="{{ old('tanggal_lahir', $anggota->tanggal_lahir?->format('Y-m-d')) }}"
                                   max="{{ date('Y-m-d') }}">
                            @error('tanggal_lahir')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Jenis Kelamin --}}
                        <div class="col-md-4 mb-3">
                            <label for="jenis_kelamin" class="form-label">
                                Jenis Kelamin <span class="text-danger">*</span>
                            </label>
                            <select name="jenis_kelamin" 
                                    id="jenis_kelamin" 
                                    class="form-select @error('jenis_kelamin') is-invalid @enderror">
                                <option value="">-- Pilih Jenis Kelamin --</option>
                                @foreach(['Laki-laki', 'Perempuan'] as $jk)
                                    <option value="{{ $jk }}" 
                                            {{ old('jenis_kelamin', $anggota->jenis_kelamin) == $jk ? 'selected' : '' }}>
                                        {{ $jk }}
                                    </option>
                                @endforeach
                            </select>
                            @error('jenis_kelamin')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Pekerjaan --}}
                        <div class="col-md-4 mb-3">
                            <label for="pekerjaan" class="form-label">Pekerjaan</label>
                            <input type="text" 
                                   name="pekerjaan" 
                                   id="pekerjaan" 
                                   class="form-control @error('pekerjaan') is-invalid @enderror"
                                   value="{{ old('pekerjaan', $anggota->pekerjaan) }}">
                            @error('pekerjaan')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <div class="row">
                        {{-- Tanggal Daftar --}}
                        <div class="col-md-6 mb-3">
                            <label for="tanggal_daftar" class="form-label">
                                Tanggal Pendaftaran <span class="text-danger">*</span>
                            </label>
                            <input type="date" 
                                   name="tanggal_daftar" 
                                   id="tanggal_daftar" 
                                   class="form-control @error('tanggal_daftar') is-invalid @enderror"
                                   value="{{ old('tanggal_daftar', $anggota->tanggal_daftar?->format('Y-m-d')) }}"
                                   max="{{ date('Y-m-d') }}">
                            @error('tanggal_daftar')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        {{-- Status --}}
                        <div class="col-md-6 mb-3">
                            <label for="status" class="form-label">
                                Status <span class="text-danger">*</span>
                            </label>
                            <select name="status" 
                                    id="status" 
                                    class="form-select @error('status') is-invalid @enderror">
                                @foreach(['Aktif', 'Nonaktif'] as $st)
                                    <option value="{{ $st }}" 
                                            {{ old('status', $anggota->status) == $st ? 'selected' : '' }}>
                                        {{ $st }}
                                    </option>
                                @endforeach
                            </select>
                            @error('status')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                    
                    <hr>
                    
                    {{-- Buttons --}}
                    <div class="d-flex justify-content-between">
                        <a href="{{ route('anggota.show', $anggota->id) }}" class="btn btn-secondary">
                            <i class="bi bi-arrow-left"></i> Kembali
                        </a>
                        <button type="submit" class="btn btn-warning">
                            <i class="bi bi-save"></i> Update Anggota
                        </button>
                    </div>
                </form>
            </div>
        </div>
        
        {{-- Info Update --}}
        <div class="card mt-3">
            <div class="card-body">
                <small class="text-muted">
                    <i class="bi bi-info-circle"></i>
                    <strong>Informasi:</strong><br />
                    - Anggota terdaftar: {{ $anggota->created_at->format('d M Y H:i') }}<br />
                    - Terakhir diupdate: {{ $anggota->updated_at->format('d M Y H:i') }}<br />
                    - Lama menjadi anggota: {{ $anggota->lama_anggota }} hari ({{ round($anggota->lama_anggota / 365, 1) }} tahun)
                </small>
            </div>
        </div>
    </div>
</div>
@endsection
 
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/id.js"></script>
<script>
    // Initialize Flatpickr
    flatpickr("#tanggal_lahir", {
        dateFormat: "Y-m-d",
        maxDate: "today",
        locale: "id",
        altInput: true,
        altFormat: "d F Y",
    });
    
    flatpickr("#tanggal_daftar", {
        dateFormat: "Y-m-d",
        maxDate: "today",
        locale: "id",
        altInput: true,
        altFormat: "d F Y",
    });
</script>
@endpush

c. Update AnggotaController - Method update()

use App\Http\Requests\UpdateAnggotaRequest;
 
/**
 * Update the specified resource in storage.
 */
public function update(UpdateAnggotaRequest $request, string $id)
{
    try {
        $anggota = Anggota::findOrFail($id);
        
        // Update anggota dengan validated data
        $anggota->update($request->validated());
        
        // Redirect dengan success message
        return redirect()->route('anggota.show', $anggota->id)
                         ->with('success', 'Data anggota berhasil diupdate!');
                         
    } catch (\Exception $e) {
        // Redirect dengan error message jika gagal
        return redirect()->back()
                         ->withInput()
                         ->with('error', 'Gagal mengupdate anggota: ' . $e->getMessage());
    }
}

d. Test Update Operation

  1. Dari halaman detail anggota, klik "Edit Anggota"
  2. URL: http://localhost:8000/anggota/1/edit
  3. Verifikasi:
    • Form ter-isi dengan data anggota yang ada
    • Date picker menampilkan tanggal dengan format Indonesia
  4. Test update:
    • Ubah beberapa field (email, telepon, status)
    • Klik "Update Anggota"
    • Verifikasi:
      • Redirect ke detail anggota
      • Flash message muncul
      • Data ter-update

6. PRAKTIKUM 5: Delete Anggota

Tujuan

Implementasi method untuk menghapus data anggota.

Langkah-langkah

a. Update AnggotaController - Method destroy()

/**
 * Remove the specified resource from storage.
 */
public function destroy(string $id)
{
    try {
        $anggota = Anggota::findOrFail($id);
        $namaAnggota = $anggota->nama;
        
        // Delete anggota
        $anggota->delete();
        
        // Redirect dengan success message
        return redirect()->route('anggota.index')
                         ->with('success', "Anggota '{$namaAnggota}' berhasil dihapus!");
                         
    } catch (\Exception $e) {
        // Redirect dengan error message jika gagal
        return redirect()->back()
                         ->with('error', 'Gagal menghapus anggota: ' . $e->getMessage());
    }
}

b. Update View Index - Delete Button sudah ada di pertemuan 11

c. Update View Show - Delete Button sudah ada di pertemuan 11

d. Test Delete Operation

  1. Test delete dari index atau detail page
  2. Verifikasi confirmation dialog muncul
  3. Verifikasi anggota terhapus dan flash message muncul

E. TUGAS

Tugas 1: Auto-Generate Kode Anggota (30%)

Instruksi: Implementasi auto-generate kode anggota dengan format: AGT-[TAHUN]-[NOMOR_URUT]

Spesifikasi:

  1. Helper Function di AnggotaController:
private function generateKodeAnggota()
{
    $tahun = date('Y');
    $lastAnggota = Anggota::whereYear('created_at', $tahun)
                          ->orderBy('kode_anggota', 'desc')
                          ->first();
    
    if ($lastAnggota) {
        $lastNumber = intval(substr($lastAnggota->kode_anggota, -3));
        $newNumber = $lastNumber + 1;
    } else {
        $newNumber = 1;
    }
    
    return 'AGT-' . $tahun . '-' . str_pad($newNumber, 3, '0', STR_PAD_LEFT);
}
  1. Update create() method:
public function create()
{
    $kodeAnggota = $this->generateKodeAnggota();
    return view('anggota.create', compact('kodeAnggota'));
}
  1. Update View:
<input type="text" 
       name="kode_anggota" 
       class="form-control" 
       value="{{ old('kode_anggota', $kodeAnggota) }}" 
       readonly>

Tugas 2: Export Anggota ke Excel (40%)

Instruksi: Implementasikan fitur export data anggota ke file Excel menggunakan package Laravel Excel (maatwebsite/excel) versi terbaru.

Spesifikasi:

  1. Install Package:
composer require maatwebsite/excel
  1. Buat Export Class:
php artisan make:export AnggotaExport --model=Anggota
  1. Isi Export Class: File: app/Exports/AnggotaExport.php
namespace App\Exports;
 
use App\Models\Anggota;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
 
class AnggotaExport implements FromCollection, WithHeadings
{
    public function collection()
    {
        return Anggota::select([
            'kode_anggota', 'nama', 'email', 'telepon', 'alamat',
            'tanggal_lahir', 'jenis_kelamin', 'pekerjaan', 'status', 'tanggal_daftar',
        ])->get();
    }
 
    public function headings(): array
    {
        return [
            'Kode', 'Nama', 'Email', 'Telepon', 'Alamat',
            'Tanggal Lahir', 'Jenis Kelamin', 'Pekerjaan', 'Status', 'Tanggal Daftar',
        ];
    }
}
  1. Controller Method:
use App\Exports\AnggotaExport;
use Maatwebsite\Excel\Facades\Excel;
 
public function export()
{
    return Excel::download(new AnggotaExport, 'anggota_' . date('Y-m-d_His') . '.xlsx');
}
  1. Button di Index:
<a href="{{ route('anggota.export') }}" class="btn btn-success">
    <i class="bi bi-file-excel"></i> Export Excel
</a>

Referensi:

Catatan:

  • Jangan gunakan box/spout atau SimpleExcel karena sudah tidak direkomendasikan dan tidak lagi dikembangkan.

Tugas 3: Advanced Search & Filter (30%)

Instruksi: Tambahkan fitur search dan filter advanced untuk anggota.

Spesifikasi:

  1. Form Search di Index:
<form action="{{ route('anggota.search') }}" method="GET">
    <div class="row">
        <div class="col-md-3">
            <input type="text" name="keyword" class="form-control" 
                   placeholder="Cari nama/email/telepon">
        </div>
        <div class="col-md-2">
            <select name="jenis_kelamin" class="form-select">
                <option value="">Semua Jenis Kelamin</option>
                <option value="Laki-laki">Laki-laki</option>
                <option value="Perempuan">Perempuan</option>
            </select>
        </div>
        <div class="col-md-2">
            <select name="status" class="form-select">
                <option value="">Semua Status</option>
                <option value="Aktif">Aktif</option>
                <option value="Nonaktif">Nonaktif</option>
            </select>
        </div>
        <div class="col-md-2">
            <select name="pekerjaan" class="form-select">
                <option value="">Semua Pekerjaan</option>
                <option value="Mahasiswa">Mahasiswa</option>
                <option value="Pegawai">Pegawai</option>
                <option value="Wiraswasta">Wiraswasta</option>
            </select>
        </div>
        <div class="col-md-3">
            <button type="submit" class="btn btn-primary">
                <i class="bi bi-search"></i> Cari
            </button>
            <a href="{{ route('anggota.index') }}" class="btn btn-secondary">
                <i class="bi bi-x"></i> Reset
            </a>
        </div>
    </div>
</form>
  1. Controller Method:
public function search(Request $request)
{
    $query = Anggota::query();
    
    if ($request->keyword) {
        $query->where(function($q) use ($request) {
            $q->where('nama', 'like', '%' . $request->keyword . '%')
              ->orWhere('email', 'like', '%' . $request->keyword . '%')
              ->orWhere('telepon', 'like', '%' . $request->keyword . '%');
        });
    }
    
    if ($request->jenis_kelamin) {
        $query->where('jenis_kelamin', $request->jenis_kelamin);
    }
    
    if ($request->status) {
        $query->where('status', $request->status);
    }
    
    if ($request->pekerjaan) {
        $query->where('pekerjaan', $request->pekerjaan);
    }
    
    $anggotas = $query->latest()->get();
    
    // Statistics
    $totalAnggota = $anggotas->count();
    $anggotaAktif = $anggotas->where('status', 'Aktif')->count();
    $anggotaNonaktif = $anggotas->where('status', 'Nonaktif')->count();
    
    return view('anggota.index', compact(
        'anggotas',
        'totalAnggota',
        'anggotaAktif',
        'anggotaNonaktif'
    ));
}

Submission:

  • Format: Link repository GitHub (sertakan screenshot di README)
  • Deadline: Pertemuan 14
  • Upload ke: Ngaji UIN Gusdur (submit link repository GitHub)

F. EVALUASI

1. Kuis Singkat

Pilihan Ganda:

  1. DRY principle artinya:

    • A. Do Repeat Yourself
    • B. Don't Repeat Yourself
    • C. Don't Run Yesterday
    • D. Do Repeat Yesterday
  2. Validation regex untuk nomor telepon Indonesia:

    • A. /^08[0-9]{8,11}$/
    • B. /^(\+62|62|0)[0-9]{9,12}$/
    • C. /^[0-9]{10,13}$/
    • D. Semua benar
  3. Validation untuk tanggal lahir harus:

    • A. before:today
    • B. after:today
    • C. equal:today
    • D. between:today
  4. Format date untuk MySQL adalah:

    • A. d-m-Y
    • B. Y-m-d
    • C. m/d/Y
    • D. d/m/Y
  5. Accessor di Model untuk umur menggunakan Carbon:

    • A. $this->tanggal_lahir->age
    • B. $this->tanggal_lahir->year
    • C. $this->tanggal_lahir->old
    • D. $this->tanggal_lahir->calculate
  6. Flatpickr digunakan untuk:

    • A. Pilih warna
    • B. Pilih file
    • C. Pilih tanggal
    • D. Pilih gambar
  7. Validation untuk email unique saat update:

    • A. unique:anggota,email
    • B. unique:anggota,email,$id
    • C. unique:anggota
    • D. email|unique
  8. Form Request disimpan di folder:

    • A. app/Requests
    • B. app/Http/Requests
    • C. app/Forms
    • D. resources/Requests
  9. Method untuk ignore current record di unique validation:

    • A. Skip current
    • B. Except
    • C. Ignore
    • D. Tambah ID di akhir
  10. Casting date di Model menggunakan:

    • A. protected $dates = []
    • B. protected $casts = ['field' => 'date']
    • C. protected $dateFormat
    • D. protected $dateFields

Essay:

  1. Jelaskan apa itu DRY principle dan berikan 3 contoh penerapannya di Laravel! (15 poin)

  2. Apa perbedaan validation untuk create dan update pada field unique? Jelaskan dengan contoh code! (15 poin)

  3. Buatlah validation rules untuk field "telepon" dengan regex format Indonesia (08xxxxxxxxxx)! (10 poin)

  4. Jelaskan cara handle date input di Laravel dari form hingga disimpan ke database! (15 poin)

  5. Apa keuntungan menggunakan Flatpickr dibanding HTML5 date input? (10 poin)


2. Checklist Kompetensi

NoKompetensiBelumCukupMahir
1Menerapkan DRY principle○○○
2Form Request validation○○○
3Advanced validation rules○○○
4Handle date input○○○
5Regex validation○○○
6CRUD consistency○○○
7Email validation○○○
8Unique validation update○○○
9Date picker implementation○○○
10Code organization○○○

G. REFERENSI

1. Dokumentasi Laravel 12

2. Libraries

3. Regex Reference


H. PERSIAPAN PERTEMUAN 14

Topik: Authentication & Transaksi Peminjaman

Preview:

  1. Laravel Breeze installation
  2. Login & Register system
  3. Middleware authentication
  4. Database relationships
  5. Transaksi peminjaman buku

Yang Perlu Disiapkan:

  1. CRUD Buku dan Anggota sudah lengkap
  2. Pahami relasi database (hasMany, belongsTo)
  3. Study Laravel authentication

Pre-reading:


Selamat Belajar! 🚀đŸ‘Ĩ

End of Module - Pertemuan 13

Next: Pertemuan 14 - Authentication & Transaksi Peminjaman