Skip to content

Day 16 โ€” Modul Peminjaman (Loan): Create & List โ€‹

Fase: 2 โ€” Mini Project Asset Management | Minggu: 4 | Hari: Senin


๐ŸŽฏ Tujuan Hari Ini โ€‹

Tim membangun modul peminjaman aset dari nol: migration tabel loans, model, controller, form peminjaman, dan list riwayat peminjaman. Termasuk logika pengurangan stok aset secara otomatis.


๐Ÿ“‹ Task Wajib โ€‹

1. Standup & Briefing Minggu Terakhir (09:00 โ€“ 09:20) โ€‹

Ini adalah minggu terakhir! Bahas:

  • Review hasil demo Day 15
  • Pastikan tidak ada bug yang tertinggal dari minggu lalu
  • Klarifikasi fitur yang akan dibangun hari ini

Aturan akhir minggu:

  • Semua fitur wajib selesai sebelum Day 20
  • QA wajib membuat test case riil setiap hari
  • PM membuat to-do list harian di GitHub Issues

2. Buat Migration Tabel loans โ€‹

bash
php artisan make:migration create_loans_table

Isi method up() di file migration yang baru:

php
public function up(): void
{
    Schema::create('loans', function (Blueprint $table) {
        $table->id();

        $table->foreignId('asset_id')
              ->constrained('assets')
              ->onDelete('restrict');  // Aset tidak bisa dihapus jika masih ada pinjaman

        $table->string('borrower_name', 150)->comment('Nama peminjam');
        $table->string('borrower_department', 100)->nullable()->comment('Divisi / kelas peminjam');
        $table->date('borrow_date')->comment('Tanggal peminjaman');
        $table->date('expected_return_date')->comment('Rencana tanggal pengembalian');
        $table->date('actual_return_date')->nullable()->comment('Tanggal aktual dikembalikan');
        $table->unsignedInteger('quantity')->default(1)->comment('Jumlah yang dipinjam');
        $table->enum('status', ['dipinjam', 'dikembalikan'])->default('dipinjam');
        $table->text('notes')->nullable()->comment('Catatan tambahan');

        $table->timestamps();
    });
}
bash
php artisan migrate

Verifikasi di phpMyAdmin: tabel loans muncul dengan semua kolom dan foreign key ke assets.

3. Buat Model Loan โ€‹

bash
php artisan make:model Loan

Isi app/Models/Loan.php:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Loan extends Model
{
    protected $fillable = [
        'asset_id',
        'borrower_name',
        'borrower_department',
        'borrow_date',
        'expected_return_date',
        'actual_return_date',
        'quantity',
        'status',
        'notes',
    ];

    protected $casts = [
        'borrow_date'          => 'date',
        'expected_return_date' => 'date',
        'actual_return_date'   => 'date',
    ];

    // Relasi: 1 loan milik 1 aset
    public function asset()
    {
        return $this->belongsTo(Asset::class);
    }

    // Helper: apakah pinjaman ini sudah terlambat dikembalikan?
    public function isTerlambat(): bool
    {
        return $this->status === 'dipinjam'
            && $this->expected_return_date->isPast();
    }
}

4. Buat LoanController โ€‹

bash
php artisan make:controller LoanController --resource

Isi app/Http/Controllers/LoanController.php:

php
<?php

namespace App\Http\Controllers;

use App\Models\Loan;
use App\Models\Asset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class LoanController extends Controller
{
    public function index()
    {
        $loans = Loan::with('asset.category')
                     ->orderBy('created_at', 'desc')
                     ->paginate(15);

        return view('loans.index', compact('loans'));
    }

    public function create()
    {
        // Hanya tampilkan aset yang stoknya > 0
        $assets = Asset::where('stock', '>', 0)
                       ->with('category')
                       ->orderBy('name')
                       ->get();

        return view('loans.create', compact('assets'));
    }

    public function store(Request $request)
    {
        $request->validate([
            'asset_id'             => 'required|exists:assets,id',
            'borrower_name'        => 'required|min:3|max:150',
            'borrower_department'  => 'nullable|max:100',
            'borrow_date'          => 'required|date',
            'expected_return_date' => 'required|date|after_or_equal:borrow_date',
            'quantity'             => 'required|integer|min:1',
            'notes'                => 'nullable|max:500',
        ]);

        $asset = Asset::findOrFail($request->asset_id);

        // Validasi stok cukup
        if ($request->quantity > $asset->stock) {
            return back()
                ->withInput()
                ->withErrors([
                    'quantity' => "Stok tidak cukup! Stok tersedia: {$asset->stock} unit, kamu minta: {$request->quantity} unit."
                ]);
        }

        // Gunakan DB Transaction untuk keamanan
        DB::transaction(function () use ($request, $asset) {
            // 1. Buat record peminjaman
            Loan::create([
                'asset_id'             => $request->asset_id,
                'borrower_name'        => $request->borrower_name,
                'borrower_department'  => $request->borrower_department,
                'borrow_date'          => $request->borrow_date,
                'expected_return_date' => $request->expected_return_date,
                'quantity'             => $request->quantity,
                'status'               => 'dipinjam',
                'notes'                => $request->notes,
            ]);

            // 2. Kurangi stok aset
            $asset->decrement('stock', $request->quantity);
        });

        return redirect()->route('loans.index')
            ->with('success', "Peminjaman aset '{$asset->name}' berhasil dicatat!");
    }

    public function show(Loan $loan)
    {
        $loan->load('asset.category');
        return view('loans.show', compact('loan'));
    }

    // edit, update, destroy akan diimplementasikan jika waktu memungkinkan
}

DB::transaction() โ€” Ini sangat penting! Jika Loan::create() berhasil tapi $asset->decrement() gagal (karena error), maka seluruh operasi akan di-rollback otomatis. Data tidak akan setengah tersimpan.

5. Daftarkan Route โ€‹

Di routes/web.php:

php
use App\Http\Controllers\LoanController;

Route::resource('loans', LoanController::class)->only(['index', 'create', 'store', 'show']);

6. Buat Views Peminjaman โ€‹

resources/views/loans/index.blade.php:

html
@extends('layouts.app')
@section('title', 'Riwayat Peminjaman')
@section('page-title', 'Riwayat Peminjaman')

@section('content')
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
    <div>
        <h1>๐Ÿ“‹ Riwayat Peminjaman</h1>
        <p style="color:#666;">Total: {{ $loans->total() }} transaksi</p>
    </div>
    <a href="{{ route('loans.create') }}" class="btn-primary">+ Catat Peminjaman Baru</a>
</div>

@include('partials.alert')

<div style="background:white; border-radius:10px; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.07);">
    <table style="width:100%; border-collapse:collapse;">
        <thead>
            <tr style="background:#1a1a2e; color:white;">
                <th style="padding:12px 16px; text-align:left;">Peminjam</th>
                <th style="padding:12px 16px; text-align:left;">Aset</th>
                <th style="padding:12px 16px; text-align:center;">Qty</th>
                <th style="padding:12px 16px; text-align:center;">Tgl Pinjam</th>
                <th style="padding:12px 16px; text-align:center;">Rencana Kembali</th>
                <th style="padding:12px 16px; text-align:center;">Status</th>
                <th style="padding:12px 16px; text-align:center;">Aksi</th>
            </tr>
        </thead>
        <tbody>
            @forelse($loans as $loan)
            <tr style="border-bottom:1px solid #f0f0f0; {{ $loan->isTerlambat() ? 'background:#fff8f0;' : '' }}">
                <td style="padding:12px 16px;">
                    <strong>{{ $loan->borrower_name }}</strong>
                    @if($loan->borrower_department)
                    <br><small style="color:#999;">{{ $loan->borrower_department }}</small>
                    @endif
                </td>
                <td style="padding:12px 16px;">
                    {{ $loan->asset->name }}
                    <br><small style="color:#4f46e5; font-family:monospace;">{{ $loan->asset->code }}</small>
                </td>
                <td style="padding:12px 16px; text-align:center; font-weight:bold;">{{ $loan->quantity }}</td>
                <td style="padding:12px 16px; text-align:center;">{{ $loan->borrow_date->format('d M Y') }}</td>
                <td style="padding:12px 16px; text-align:center;">
                    {{ $loan->expected_return_date->format('d M Y') }}
                    @if($loan->isTerlambat())
                        <br><small style="color:#dc3545;">โš ๏ธ Terlambat!</small>
                    @endif
                </td>
                <td style="padding:12px 16px; text-align:center;">
                    @if($loan->status === 'dipinjam')
                        <span style="background:#fff3cd; color:#856404; padding:4px 12px; border-radius:20px; font-size:12px;">
                            ๐Ÿ”„ Dipinjam
                        </span>
                    @else
                        <span style="background:#d4edda; color:#155724; padding:4px 12px; border-radius:20px; font-size:12px;">
                            โœ… Dikembalikan
                        </span>
                    @endif
                </td>
                <td style="padding:12px 16px; text-align:center;">
                    <a href="{{ route('loans.show', $loan->id) }}" style="color:#0066cc; font-size:13px;">Detail</a>
                </td>
            </tr>
            @empty
            <tr>
                <td colspan="7" style="text-align:center; padding:40px; color:#999;">
                    Belum ada transaksi peminjaman.
                </td>
            </tr>
            @endforelse
        </tbody>
    </table>
</div>
<div style="margin-top:20px;">{{ $loans->links() }}</div>
@endsection

resources/views/loans/create.blade.php:

html
@extends('layouts.app')
@section('title', 'Catat Peminjaman')
@section('page-title', 'Catat Peminjaman Baru')

@section('content')
<form method="POST" action="{{ route('loans.store') }}"
      style="background:white; padding:30px; border-radius:10px; max-width:700px; box-shadow:0 2px 8px rgba(0,0,0,0.07);">
    @csrf

    <div style="display:grid; grid-template-columns:1fr 1fr; gap:20px;">

        <div style="grid-column:1/-1; margin-bottom:4px;">
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Aset yang Dipinjam *</label>
            <select name="asset_id" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;">
                <option value="">-- Pilih Aset --</option>
                @foreach($assets as $asset)
                <option value="{{ $asset->id }}" {{ old('asset_id') == $asset->id ? 'selected' : '' }}>
                    [{{ $asset->code }}] {{ $asset->name }} โ€” {{ $asset->category->name }}
                    (Stok: {{ $asset->stock }})
                </option>
                @endforeach
            </select>
            @error('asset_id') <span style="color:red; font-size:13px;">{{ $message }}</span> @enderror
        </div>

        <div>
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Nama Peminjam *</label>
            <input type="text" name="borrower_name" value="{{ old('borrower_name') }}"
                   style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;"
                   placeholder="Nama lengkap peminjam">
            @error('borrower_name') <span style="color:red; font-size:13px;">{{ $message }}</span> @enderror
        </div>

        <div>
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Divisi / Departemen</label>
            <input type="text" name="borrower_department" value="{{ old('borrower_department') }}"
                   style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;"
                   placeholder="Opsional">
        </div>

        <div>
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Jumlah Dipinjam *</label>
            <input type="number" name="quantity" value="{{ old('quantity', 1) }}" min="1"
                   style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;">
            @error('quantity') <span style="color:red; font-size:13px;">{{ $message }}</span> @enderror
        </div>

        <div>
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Tanggal Pinjam *</label>
            <input type="date" name="borrow_date" value="{{ old('borrow_date', date('Y-m-d')) }}"
                   style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;">
            @error('borrow_date') <span style="color:red; font-size:13px;">{{ $message }}</span> @enderror
        </div>

        <div>
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Rencana Tanggal Kembali *</label>
            <input type="date" name="expected_return_date" value="{{ old('expected_return_date') }}"
                   style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;">
            @error('expected_return_date') <span style="color:red; font-size:13px;">{{ $message }}</span> @enderror
        </div>

        <div style="grid-column:1/-1;">
            <label style="display:block; font-weight:bold; margin-bottom:6px;">Catatan Tambahan</label>
            <textarea name="notes" rows="3"
                      style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; resize:vertical;"
                      placeholder="Kondisi aset saat dipinjam, tujuan pemakaian, dll">{{ old('notes') }}</textarea>
        </div>
    </div>

    <div style="display:flex; gap:10px; margin-top:20px;">
        <button type="submit" style="background:#1a1a2e; color:white; padding:12px 30px; border:none; border-radius:6px; cursor:pointer; font-size:15px;">
            ๐Ÿ“‹ Simpan Peminjaman
        </button>
        <a href="{{ route('loans.index') }}" style="padding:12px 20px; border:1px solid #ddd; border-radius:6px; text-decoration:none; color:#333;">
            Batal
        </a>
    </div>
</form>
@endsection

7. Update Sidebar Layout โ€‹

Update layouts/app.blade.php โ€” aktifkan link Peminjaman:

html
<a href="{{ route('loans.index') }}" class="{{ request()->is('loans*') ? 'active' : '' }}">
    ๐Ÿ“‹ Peminjaman
</a>

๐Ÿ“ Laporan ke Mentor โ€‹

๐Ÿ“Œ LAPORAN HARIAN โ€” Day 16
Nama     : [Nama Lengkap]
Role     : [Role Kamu]

โœ… Yang saya kerjakan hari ini:
- [ ] Migration tabel loans berhasil dibuat dan dijalankan
- [ ] Model Loan dengan relasi belongsTo Asset selesai
- [ ] LoanController dengan method index(), create(), store() selesai
- [ ] Validasi stok (quantity > stock) berjalan
- [ ] DB::transaction() diterapkan di method store()
- [ ] Halaman list peminjaman menampilkan data
- [ ] Form peminjaman bisa submit dan mengurangi stok aset

๐Ÿ“ธ Screenshot Wajib:
1. phpMyAdmin: struktur tabel loans
2. Form peminjaman dengan dropdown aset (tampil stok tersedia)
3. Setelah submit: halaman list peminjaman + stok aset berkurang di halaman /assets

๐Ÿงช QA Test:
| Test Case | Expected | Pass/Fail |
|-----------|----------|-----------|
| Pinjam 2 unit, stok awal 5 | Stok jadi 3 | |
| Pinjam lebih dari stok | Muncul pesan error | |
| Pinjam aset stok 0 (tidak muncul di dropdown) | Tidak bisa dipilih | |
| Return date sebelum borrow date | Muncul validasi error | |

โ“ Kendala:
[Tulis jika ada, khususnya tentang DB Transaction]

Catatan Mentor

DB::transaction() adalah salah satu konsep paling penting di programming backend. Pastikan peserta memahami mengapa ini diperlukan: jika server mati di tengah proses, kita tidak mau situasi di mana record Loan sudah terbuat tapi stok belum berkurang (atau sebaliknya). Transaction menjamin atomicity โ€” semua berhasil, atau tidak ada yang tersimpan.

Program Magang SMK RPL โ€” Rekayasa Perangkat Lunak