# Loan List Sisa Pinjaman + Sorting Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Menambah sisa pinjaman di loan list card, sorting by `disbursedAt` (default) dan `remaining_balance`, plus filter status verification.

**Architecture:** Backend menambah kolom `remaining_balance` di tabel `loans` dengan sync trigger saat payment. Flutter menambah field + UI display. Sorting handled di backend via kolom indexed.

**Tech Stack:** Laravel (ksu-app) + Flutter (ksu_mobile_app)

---

## Task 1: Migration — Add `remaining_balance` Column

**Files:**
- Create: `database/migrations/2026_05_20_000001_add_remaining_balance_to_loans_table.php`

- [ ] **Step 1: Create migration**

```bash
php artisan make:migration add_remaining_balance_to_loans_table --table=loans
```

- [ ] **Step 2: Edit migration file** `database/migrations/2026_05_20_*_add_remaining_balance_to_loans_table.php`

```php
<?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('loans', function (Blueprint $table) {
            $table->bigInteger('remaining_balance')->default(0)->index()->after('customer_paid_amount');
        });
    }

    public function down(): void
    {
        Schema::table('loans', function (Blueprint $table) {
            $table->dropColumn('remaining_balance');
        });
    }
};
```

- [ ] **Step 3: Run migration**

```bash
php artisan migrate
```

- [ ] **Step 4: Commit**

```bash
git add database/migrations/2026_05_20_*_add_remaining_balance_to_loans_table.php
git commit -m "feat: add remaining_balance column to loans table"
```

---

## Task 2: Artisan Command — Backfill Data Lama

**Files:**
- Create: `app/Console/Commands/BackfillRemainingBalance.php`
- Modify: `app/Console/Kernel.php` (register command)

- [ ] **Step 1: Create command**

```bash
php artisan make:command BackfillRemainingBalance
```

- [ ] **Step 2: Edit command file** `app/Console/Commands/BackfillRemainingBalance.php`

```php
<?php

namespace App\Console\Commands;

use App\Models\Loan;
use App\Models\RepaymentSchedule;
use Illuminate\Console\Command;

class BackfillRemainingBalance extends Command
{
    protected $signature = 'loans:backfill-remaining-balance';
    protected $description = 'Backfill remaining_balance for all existing loans';

    public function handle(): int
    {
        $this->info('Starting backfill of remaining_balance...');

        $loans = Loan::with('repaymentSchedules')->get();
        $bar = $this->output->createProgressBar($loans->count());
        $bar->start();

        foreach ($loans as $loan) {
            if ($loan->status === 'activated') {
                $paidSum = $loan->repaymentSchedules->sum('paid_amount');
                $remainingBalance = (int) $loan->customer_paid_amount - $paidSum;
            } else {
                // Non-activated loans: remaining = 0 (belum aktif, belum ada outstanding)
                $remainingBalance = 0;
            }

            $loan->update(['remaining_balance' => max(0, $remainingBalance)]);
            $bar->advance();
        }

        $bar->finish();
        $this->newLine();
        $this->info('Backfill completed!');

        return Command::SUCCESS;
    }
}
```

- [ ] **Step 3: Register command** `app/Console/Kernel.php` — tambahkan ke method `commands()`:

```php
protected function commands(): void
{
    // ... existing
    $this->load(__DIR__.'/Commands');
}
```

- [ ] **Step 4: Run backfill command**

```bash
php artisan loans:backfill-remaining-balance
```

- [ ] **Step 5: Commit**

```bash
git add app/Console/Commands/BackfillRemainingBalance.php
git commit -m "feat: add BackfillRemainingBalance artisan command"
```

---

## Task 3: Sync Trigger — Update `remaining_balance` on Payment

**Files:**
- Modify: `app/Services/RepaymentServiceImpl.php` (3 payment entry points)

- [ ] **Step 1: Add helper method** Tambah di `RepaymentServiceImpl` (setelah `__construct`):

```php
private function syncRemainingBalance(Loan $loan): void
{
    $remainingBalance = $this->getOutstandingLoanAmount($loan);
    $loan->update(['remaining_balance' => $remainingBalance]);
}
```

- [ ] **Step 2: Call after `doExactPayment`** — di `doExactPayment()`, sebelum `return`:

```php
// Sync remaining_balance after payment
$this->syncRemainingBalance($repayment_schedule->loan);
```

- [ ] **Step 3: Call after `doInsufficientPayment`** — cek `doInsufficientPayment` method, tambahkan call sebelum return
- [ ] **Step 4: Call after `doMultiplePayment`** — cek `doMultiplePayment` method, tambahkan call sebelum return
- [ ] **Step 5: Call in `rollbackPayment`** — di method `rollbackPayment()`, setelah loop `$schedules` dan sebelum `return`:

```php
// Sync remaining_balance after rollback
$this->syncRemainingBalance($loan->fresh());
```

- [ ] **Step 6: Call in `earlyTerminationPayment`** — di `earlyTerminationPayment()`, sebelum `return` array

- [ ] **Step 7: Verify file compiles**

```bash
php artisan tinker --execute="echo 'OK'"
```

- [ ] **Step 8: Commit**

```bash
git add app/Services/RepaymentServiceImpl.php
git commit -m "feat: sync remaining_balance on every payment/rollback event"
```

---

## Task 4: Backend API — Transform + Sorting + Default Sort

**Files:**
- Modify: `app/Services/LoanServiceImpl.php`

- [ ] **Step 1: Add `remaining_balance` to transform** Di `getAllLoans()`, tambahkan di dalam transform closure:

```php
$loan->remainingBalance = $loan->remaining_balance;
```

- [ ] **Step 2: Add `remaining_balance` sort case** Di `applyFilters()`, switch statement (setelah `case 'amount'`):

```php
case 'remaining_balance':
    $query->orderBy('loans.remaining_balance', $sortOrder);
    break;
```

- [ ] **Step 3: Add `remaining_balance` sort case** Di `applyFiltersForJoinQuery()`, switch statement (jika ada). Kalau tidak ada switch, skip这一步.

- [ ] **Step 4: Change default sort to `disbursed_at`** Di `getAllLoans()`, ubah default ORDER BY:

**Sebelum:**
```php
->orderBy('created_at', 'desc')
```

**Sesudah:**
```php
->orderByRaw("CASE WHEN disbursed_at IS NOT NULL THEN 0 ELSE 1 END, disbursed_at DESC")
```

Juga update di branch `sortByName` query (baris yang sama).

- [ ] **Step 5: Test endpoint manually**

```bash
php artisan serve --port=8000
# Di terminal lain:
curl "http://localhost:8000/api/loans?sortBy=remaining_balance&sortOrder=asc" | jq '.data[0].remainingBalance'
```

- [ ] **Step 6: Commit**

```bash
git add app/Services/LoanServiceImpl.php
git commit -m "feat: expose remaining_balance in getAllLoans + sort support"
```

---

## Task 5: Flutter — Loan Model Update

**Files:**
- Modify: `lib/models/loan.dart`
- Run: `dart run build_runner build --delete-conflicting-outputs`

- [ ] **Step 1: Add `remainingBalance` field** Di class `Loan`, setelah `customerPaidAmount`:

```dart
Decimal? remainingBalance;
```

- [ ] **Step 2: Run build_runner to regenerate mapper**

```bash
cd /Users/vendywira/Code/ksu/ksu_mobile_app && dart run build_runner build --delete-conflicting-outputs
```

- [ ] **Step 3: Commit**

```bash
git add lib/models/loan.dart lib/models/loan.mapper.dart
git commit -m "feat(flutter): add remainingBalance field to Loan model"
```

---

## Task 6: Flutter — UI Card Display + Sort Options + Default Sort

**Files:**
- Modify: `lib/fragment/LoanFragment.dart`

- [ ] **Step 1: Default sort to `disbursed_at`** Di `_applyInitialFilters()`:

```dart
sortBy: 'disbursed_at',  // berubah dari null/'created_at'
sortOrder: 'desc',
```

- [ ] **Step 2: Add sort option "Sisa Pinjaman"** Di `_buildSortingSection()`, setelah option 'Jumlah':

```dart
ListTile(
  title: const Text('Sisa Pinjaman'),
  leading: Radio<String>(
    value: 'remaining_balance',
    groupValue: sortBy,
    onChanged: (v) {
      setModalState(() => onSortByChanged(v));
    },
  ),
  onTap: () => setModalState(() => onSortByChanged('remaining_balance')),
),
```

- [ ] **Step 3: Display remaining balance in card** Di dalam column `children: [...]` di dalam card item, setelah baris `Pinjaman` (currencyFormatter.format):

```dart
if (data.remainingBalance != null && data.remainingBalance! > Decimal.zero) ...[
  const SizedBox(height: 2),
  Row(
    children: [
      Text(
        'Sisa: ',
        style: TextStyle(
          fontSize: 11,
          color: AppThemeColors.current.textSecondary,
          fontWeight: FontWeight.normal,
        ),
      ),
      Text(
        currencyFormatter.format(data.remainingBalance!.toDouble()),
        style: TextStyle(
          fontSize: 11,
          color: AppThemeColors.current.textSecondary,
          fontWeight: FontWeight.bold,
        ),
      ),
    ],
  ),
],
```

Tempatkan persis setelah widget `Text(currencyFormatter.format(data.originalLoanAmount...))` yang menampilkan "Pinjaman: Rp X".

- [ ] **Step 4: Verify status filter visibility** Pastikan `_buildStatusFilterSection()` dipanggil untuk semua role. Cek conditional di `_showFilterBottomSheet()`:

```dart
_buildStatusFilterSection(
  tempSelectedLoanStatuses,
  setModalState,
),
```

Seharusnya tidak ada guard `if (authStore.hasRoleOrHigher(...))` di sekitarnya. Kalau ada, hapus conditionalnya agar semua role bisa lihat filter status.

- [ ] **Step 5: Hot reload test**

```bash
# Pastikan tidak ada syntax error
flutter analyze lib/fragment/LoanFragment.dart
```

- [ ] **Step 6: Commit**

```bash
git add lib/fragment/LoanFragment.dart
git commit -m "feat(flutter): display remaining balance in loan list card + sort options"
```

---

## Task 7: End-to-End Verification

**Files:** (tidak ada file baru, hanya testing)

- [ ] **Step 1: Backend test — sort by remaining_balance**

```bash
curl "http://localhost:8000/api/loans?sortBy=remaining_balance&sortOrder=asc" | jq '.data | length'
```

- [ ] **Step 2: Backend test — default sort disbursed_at**

```bash
curl "http://localhost:8000/api/loans" | jq '.data[0].disbursedAt'
```

- [ ] **Step 3: Backend test — remaining_balance is exposed**

```bash
curl "http://localhost:8000/api/loans" | jq '.data[0].remainingBalance'
```

- [ ] **Step 4: Flutter test — UI hot reload**

Buka app, navigate ke loan list, tap filter icon → pastikan status FilterChips muncul dan multi-select berfungsi. Pastikan "Sisa Pinjaman" muncul di opsi sorting.

- [ ] **Step 5: Payment flow test**

1. Buat loan → activate → bayar cicilan
2. Cek `remaining_balance` di database
3. Cek loan list UI — sisa seharusnya berkurang

---

## Self-Review Checklist

- [ ] Migration menambahkan kolom `remaining_balance` dengan index
- [ ] Artisan command backfill semua data lama
- [ ] Sync trigger di: `doExactPayment`, `doInsufficientPayment`, `doMultiplePayment`, `rollbackPayment`, `earlyTerminationPayment`
- [ ] Transform expose `remainingBalance` di response
- [ ] Sort `remaining_balance` supported di backend
- [ ] Default sort berubah ke `disbursed_at DESC` (activated loans lebih dulu)
- [ ] Flutter model punya `remainingBalance`
- [ ] Flutter UI render baris "Sisa: Rp X" di bawah "Pinjaman: Rp X"
- [ ] Flutter sort option "Sisa Pinjaman" tersedia
- [ ] Status filter visible untuk semua role
- [ ] No placeholder / TODO di kode implementasi