# Activity Log Module — Dokumentasi Penuh

## 1. Pengenalan

Activity Log module merekod dan memaparkan setiap aktiviti penting dalam sistem — bila user create, update, delete data, dan siapa yang buat. Module ni guna package **Spatie Activitylog** (`spatie/laravel-activitylog`).

---

## 2. Struktur Database

**Table:** `activity_log`

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint (PK) | Primary key |
| `log_name` | string (nullable) | Nama kumpulan log (contoh: `Backend User`, `Frontend User`) |
| `description` | text | Penerangan aktiviti |
| `subject_type` | string (nullable) | Model class yang kena action (polymorphic) |
| `subject_id` | bigint (nullable) | ID record yang kena action |
| `event` | string (nullable) | Jenis event: `created`, `updated`, `deleted`, `login`, dll |
| `causer_type` | string (nullable) | Model class yang buat action (polymorphic) |
| `causer_id` | bigint (nullable) | ID user yang buat action |
| `attribute_changes` | json (nullable) | Senarai field yang berubah (old vs new) |
| `properties` | json (nullable) | Data tambahan (nullable) |
| `url` | string (nullable) | URL tempat action berlaku |
| `ip_address` | string(45) (nullable) | IP pengguna (support IPv6) |
| `user_agent` | text (nullable) | Browser/device info |
| `created_at` | timestamp | Bila log direkod |
| `updated_at` | timestamp | Bila log dikemaskini |

**Indexes:**
- `log_name` (index)
- `subject_type + subject_id` (morph index)
- `causer_type + causer_id` (morph index)
- `ip_address` (index)
- `created_at` (index)

---

## 3. Aliran Data (Data Flow)

```
┌────────────────────────────────────────────────────────────────────────┐
│                       AUTO LOGGING (Spatie)                          │
│                                                                      │
│  BackendUser model ──LogsActivity trait──> Spatie Activitylog         │
│  FrontendUser model ──LogsActivity trait──> Spatie Activitylog        │
│                                                                      │
│  Setiap kali model di-create/update/delete, Spatie auto-rekod:       │
│    - subject_type, subject_id (model & record ID)                    │
│    - event (created/updated/deleted)                                 │
│    - description (default: "This model has been {event}")            │
│    - attribute_changes (field yang berubah)                           │
└────────────────────────────────┬──────────────────────────────────────┘
                                 │
                                 ▼
┌────────────────────────────────────────────────────────────────────────┐
│                      ACTIVITY OBSERVER                                 │
│                                                                      │
│  ActivityObserver@creating() dipanggil SEBELUM activity disimpan:     │
│    - Set url (current request URL)                                    │
│    - Set ip_address (client IP)                                       │
│    - Set user_agent (browser)                                         │
│    - Determine causer berdasarkan auth guard (admin/user)             │
│    - Set log_name berdasarkan causer_type                             │
└────────────────────────────────┬──────────────────────────────────────┘
                                 │
                                 ▼
                       ┌─────────────────────┐
                       │   activity_log table │
                       └─────────────────────┘
                                 │
                                 ▼
┌────────────────────────────────────────────────────────────────────────┐
│                      DISPLAY DI BROWSER                                │
│                                                                      │
│  BackendHomeController -> index.blade.php                             │
│    - Search by: log_name, description, event                          │
│    - Paginate 10 records per page                                     │
│    - Columns: No, Log Name, Performed By, Model Name,                 │
│               Action, IP Address, User Agent, Timestamp               │
│    - Action badges: created=green, updated=orange, deleted=red        │
└────────────────────────────────────────────────────────────────────────┘
```

---

## 4. Penjelasan Setiap File

### 4.1 Package: `spatie/laravel-activitylog` (composer.json)

```
"spatie/laravel-activitylog": "^5.0"
```

Package ni yang handle:
- Auto record activity bila model diubah
- Polymorphic relationship (`subject`, `causer`)
- Batch cleanup log lama

### 4.2 Config: `config/activitylog.php`

```php
return [
    'enabled' => env('ACTIVITYLOG_ENABLED', true),
    'clean_after_days' => 365,  // Auto delete log lebih 1 tahun
    'default_log_name' => 'default',
    'activity_model' => Activity::class,
    'buffer' => [
        'enabled' => env('ACTIVITYLOG_BUFFER_ENABLED', false),
    ],
];
```

- Boleh disable total guna `ACTIVITYLOG_ENABLED=false` dalam `.env`
- Log lebih 365 hari akan di-clean (via artisan command)
- Buffer mode disabled (log terus ke DB)

### 4.3 Migration: `2026_04_22_220206_create_activity_log_table.php`

Migration standard Spatie dengan custom columns tambahan:

```php
$table->string('url')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->json('attribute_changes')->nullable();
$table->index('ip_address');
$table->index('created_at');
```

**Custom columns** (`url`, `ip_address`, `user_agent`) tak ada dalam Spatie default — ditambah khas untuk tracking detail request.

### 4.4 Observer: `app/Observers/ActivityObserver.php`

**Registered in** `AppServiceProvider@boot`:
```php
Activity::observe(ActivityObserver::class);
```

**Function `creating(Activity $activity)`:**

Observer ni auto-populate data sebelum activity disimpan:

```
Step 1: Skip jika runningInConsole() — log dari cron/queue tak perlu URL/IP
Step 2: Set url, ip_address, user_agent dari current request
Step 3: Set causer jika belum ada:
  ├─ check auth:admin guard → set BackendUser
  ├─ check auth:user guard  → set FrontendUser
  └─ fallback → guna subject sebagai causer
Step 4: Set log_name berdasarkan causer_type:
  ├─ BackendUser  → "Backend User"
  └─ FrontendUser → "Frontend User"
```

**Kenapa observer penting?**
- Spatie default TIDAK rekod URL, IP, User-Agent
- Observer ni tambah detail tu secara automatik
- Juga ensure log_name konsisten (Backend User / Frontend User)

### 4.5 Model: Tidak Ada Model Khas

Module ni **tidak** guna model custom. Terus guna `Spatie\Activitylog\Models\Activity`.

```php
use Spatie\Activitylog\Models\Activity;
```

**Eloquent relationships available:**
- `$activity->subject` — polymorphic: model yang kena action
- `$activity->causer` — polymorphic: user yang buat action
- `$activity->causer()->withoutGlobalScopes()->first()` — kalau subject dah soft-deleted

### 4.6 Model dengan LogsActivity Trait

Dua model guna `LogsActivity` trait:

#### `BackendUser`
```php
use Spatie\Activitylog\Models\Concerns\LogsActivity;

class BackendUser extends Authenticatable
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['username', 'email', 'first_name', 'last_name', 'profile_picture', 'is_active'])
            ->useLogName('Backend User')
            ->setDescriptionForEvent(fn(string $eventName) => "This model has been {$eventName}")
            ->dontLogEmptyChanges();
    }
}
```

#### `FrontendUser`
```php
use Spatie\Activitylog\Models\Concerns\LogsActivity;

class FrontendUser extends Authenticatable
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['username', 'email', 'first_name', 'last_name', 'profile_picture', 'is_active'])
            ->useLogName('Backend User')  // Note: guna 'Backend User' juga!
            ->setDescriptionForEvent(fn(string $eventName) => "This model has been {$eventName}")
            ->dontLogEmptyChanges();
    }
}
```

**Apa yang di-log:**
- Hanya field yang listed dalam `logOnly()` — bukan semua field
- Empty changes tak di-log (`dontLogEmptyChanges`)
- Description generic: "This model has been created/updated/deleted"

### 4.7 Controller: `app/Http/Controllers/Backend/ActivityLogController.php`

```php
class ActivityLogController extends Controller
{
    public function index(Request $request)
    {
        $search = $request->get('search');

        $activities = Activity::with('causer')
            ->when($search, function ($q) use ($search) {
                $q->where(function ($sub) use ($search) {
                    $sub->where('log_name', 'like', "%{$search}%")
                        ->orWhere('description', 'like', "%{$search}%")
                        ->orWhere('event', 'like', "%{$search}%");
                });
            })
            ->latest()
            ->paginate(10)->onEachSide(1)->withQueryString();

        return view('backend.module.activityLog.index', [
            'activities' => $activities,
            'search' => $search,
        ]);
    }
}
```

**Fungsi:**
- `index()` — Paginated list (10/page) dengan search on `log_name`, `description`, `event`
- Eager load `causer` relationship untuk display nama user
- Order by latest (descending)
- Ada commented-out `indexApi()` untuk JSON endpoint

**Route:**
```php
Route::get('/activity-log', [ActivityLogController::class, 'index'])
    ->middleware('permission:activity-log.view,admin')
    ->name('activity-log.index');
```

### 4.8 View: `resources/views/backend/module/activityLog/index.blade.php`

**Table columns:**

| Column | Source | Format |
|--------|--------|--------|
| No | `$loop->iteration` | Number |
| Log Name | `$activity->log_name` | String |
| Performed By | `$activity->causer` | Username + email / "User #ID (Deleted)" / "System/Automated" |
| Model Name | `$activity->causer_type` | Class name (short) |
| Action | `$activity->event` | Badge: green=created, orange=updated, red=deleted, blue=other |
| IP Address | `$activity->ip_address` | Monospace font |
| User Agent | `$activity->user_agent` | Truncated 50 chars with tooltip |
| Timestamp | `$activity->created_at` | `h:i A` + `d-m-Y` |

**Features:**
- **Search** — debounce 400ms, auto-submit, search across log_name/description/event
- **Pagination** — `vendor.pagination.custom` dengan `onEachSide(1)`
- **Empty state** — icon + "No activity logs found" message
- **Causer fallback** — handle deleted user atau system action

### 4.9 Seeders

#### `BackendMenuSeeder.php`
- Create permission: `activity-log.view`
- Add route `activity-log.index` to Backend menu
- Create menu item "Activity Log" dengan icon `fa-clock-rotate-left`

#### `AdminRolePermissionSeeder.php`
- Assign `activity-log.view` to `editor` dan `viewer` roles

#### `BackendUserSeeder.php` & `FrontendUserSeeder.php`
- Disable auto-logging semasa seeder: `activity()->disableLogging()`
- Manual create satu log entry dengan event `'seeded'`

### 4.10 Permissions

| Permission | Guard | Routes Protected |
|------------|-------|-----------------|
| `activity-log.view` | admin | `activity-log.index` |

- Permission ni **protected** — tak boleh delete dari UI (dalam `PermissionController`)
- Auto-generated dari route prefix `activity-log` → module `ActivityLog` (dalam `AutoPermissionController`)

---

## 5. Cara Guna

### 5.1 Auto Logging (Models)

Untuk enable auto logging kat mana-mana model baru:

```php
use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Activitylog\Support\LogOptions;

class YourModel extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['field1', 'field2'])
            ->useLogName('Your Module')
            ->setDescriptionForEvent(fn(string $eventName) => "This model has been {$eventName}")
            ->dontLogEmptyChanges();
    }
}
```

### 5.2 Manual Logging

Guna dalam controller/command:

```php
use Spatie\Activitylog\Models\Activity;

// Disable auto-logging
activity()->disableLogging();

// Manual log
activity()
    ->performedOn($model)
    ->causedBy($user)
    ->withProperties(['key' => 'value'])
    ->event('custom_event')
    ->log('Manual description');
```

### 5.3 Query Activities

```php
use Spatie\Activitylog\Models\Activity;

// Get recent 50 logs
$logs = Activity::with('causer')->latest()->take(50)->get();

// Filter by event
$creates = Activity::where('event', 'created')->get();

// By user
$userLogs = Activity::where('causer_type', BackendUser::class)
    ->where('causer_id', $userId)
    ->get();

// By subject model
$modelLogs = Activity::where('subject_type', YourModel::class)
    ->where('subject_id', $recordId)
    ->get();
```

### 5.4 Clean Old Logs

```bash
# Manual clean log lebih 365 hari (ikut config)
php artisan activitylog:clean
```

---

## 6. Hubungan Antara Komponen

```
┌──────────────┐     ┌──────────────────┐     ┌─────────────────────────┐
│   Routes     │────>│   Controllers    │────>│     Views               │
│  web.php     │     │                  │     │                         │
│              │     │ ActivityLogCtrl  │     │ activityLog/index.blade │
│ /activity-log│────>│   → index()      │────>│   ├─ Search             │
│              │     │   → with('causer')│     │   ├─ Table + Badges    │
│              │     │   → paginate(10)  │     │   └─ Pagination        │
└──────────────┘     └──────────────────┘     └─────────────────────────┘
                              │
                              ▼
                     ┌──────────────────┐
                     │  Spatie\Model    │
                     │ Activity (no custom model) │
                     └──────────────────┘
                              │
                              ▼
                     ┌──────────────────────────────────────────────┐
                     │  Database: activity_log                      │
                     │  ├─ Auto populated by LogsActivity trait      │
                     │  │  (BackendUser, FrontendUser)               │
                     │  ├─ Observer enrich with URL/IP/UserAgent     │
                     │  └─ Causer determined by auth guard           │
                     └──────────────────────────────────────────────┘
```

---

## 7. Tips & Troubleshooting

### Log tak direkod
- Check `.env`: `ACTIVITYLOG_ENABLED=true`
- Confirm model guna `LogsActivity` trait
- Confirm `getActivitylogOptions()` defined
- Check observer tak skip (runningInConsole?)

### Causer sentiasa null
- Observer check middleware: `auth:admin` atau `auth:user`
- Route mesti ada middleware auth yang sesuai
- Kalau console/queue, causer tak auto-set

### IP/URL/User-Agent kosong
- Observer mungkin tak register — check `AppServiceProvider@boot`
- Observer skip jika `runningInConsole()` — log dari artisan takde request context

### Log terlalu banyak
- Adjust `clean_after_days` dalam `config/activitylog.php`
- Jalankan `php artisan activitylog:clean` secara berkala (cron)
- Limit `logOnly()` dalam `getActivitylogOptions()` — jangan log semua field

### User deleted — nama tak muncul
- View ada fallback: "User #ID (Deleted)" jika causer record dah dipadam
- Guna `$activity->causer()->withoutGlobalScopes()->first()` untuk access deleted user

### Custom log_name tak set
- Observer set `log_name` berdasarkan `causer_type`
- Kalau nak guna `log_name` berbeza, set manually dalam observer atau override
