# GMP Platform — Features Guide

Dokumentasi lengkap fitur-fitur yang telah dibangun di monorepo ini.

---

## Daftar Isi

1. [packages/utils — Shared Utility Functions](#1-packagesutils--shared-utility-functions)
2. [packages/ui-shadcn — Library UI Bersama](#2-packagesui-shadcn--library-ui-bersama)
3. [packages/laravel-core — Column Mapping System](#3-packageslaravel-core--column-mapping-system)
4. [Tooling & Config](#4-tooling--config)

---

## 1. `packages/utils` — Shared Utility Functions

Package TypeScript berisi fungsi-fungsi umum yang bisa dipakai oleh semua app di monorepo.

**Package name:** `@gmp/utils`

### Instalasi / Penggunaan

Package ini sudah terhubung via npm workspaces. Import langsung dari `@gmp/utils`:

```ts
import { cn, sleep, getCookie, setCookie, removeCookie, getPageNumbers } from '@gmp/utils'
```

---

### `cn(...classes)` — Merge Tailwind Class Names

Menggabungkan class names dengan aman, mendukung conditional class dan menghindari konflik Tailwind.

```ts
import { cn } from '@gmp/utils'

// Penggabungan biasa
cn('px-4 py-2', 'bg-blue-500')
// → 'px-4 py-2 bg-blue-500'

// Conditional class
cn('btn', isActive && 'btn-active', isDisabled && 'opacity-50')
// → 'btn btn-active' (jika isActive=true, isDisabled=false)

// Menyelesaikan konflik Tailwind (tailwind-merge)
cn('px-4', 'px-8')
// → 'px-8'  (bukan 'px-4 px-8')

// Dalam komponen React
function Button({ className, disabled }: Props) {
  return (
    <button className={cn('btn-base', disabled && 'cursor-not-allowed opacity-50', className)}>
      Click
    </button>
  )
}
```

---

### `sleep(ms)` — Async Delay

Menunggu sejumlah milidetik secara async.

```ts
import { sleep } from '@gmp/utils'

// Tunggu 1 detik
await sleep(1000)

// Contoh penggunaan dalam loading state
async function fetchWithDelay() {
  setLoading(true)
  await sleep(500) // delay minimal 500ms agar UX tidak flicker
  const data = await fetchData()
  setLoading(false)
  return data
}

// Retry dengan jeda
async function retryRequest(fn: () => Promise<any>, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn()
    } catch {
      if (i < retries - 1) await sleep(1000 * (i + 1)) // backoff
    }
  }
}
```

---

### `getCookie / setCookie / removeCookie` — Cookie Management

Utilitas untuk membaca, menulis, dan menghapus cookie di browser.

```ts
import { getCookie, setCookie, removeCookie } from '@gmp/utils'

// Set cookie (expires 7 hari dari sekarang)
setCookie('token', 'abc123', 7)

// Baca cookie
const token = getCookie('token')
// → 'abc123' atau '' jika tidak ada

// Hapus cookie
removeCookie('token')

// Contoh — simpan preferensi user
setCookie('lang', 'id', 365)   // simpan 1 tahun
const lang = getCookie('lang') // → 'id'

// Contoh — logout: bersihkan semua auth cookie
function logout() {
  removeCookie('token')
  removeCookie('session')
  window.location.href = '/login'
}
```

---

### `getPageNumbers(current, total)` — Kalkulasi Halaman Pagination

Menghasilkan array nomor halaman dengan ellipsis (`null`) untuk komponen pagination.

```ts
import { getPageNumbers } from '@gmp/utils'

getPageNumbers(1, 10)
// → [1, 2, 3, null, 9, 10]

getPageNumbers(5, 10)
// → [1, null, 4, 5, 6, null, 10]

getPageNumbers(10, 10)
// → [1, 2, null, 8, 9, 10]

// Contoh dalam komponen Pagination React
function Pagination({ currentPage, totalPages, onPageChange }) {
  const pages = getPageNumbers(currentPage, totalPages)

  return (
    <nav>
      {pages.map((page, i) =>
        page === null ? (
          <span key={i}>…</span>
        ) : (
          <button
            key={page}
            className={cn('page-btn', page === currentPage && 'active')}
            onClick={() => onPageChange(page)}
          >
            {page}
          </button>
        )
      )}
    </nav>
  )
}
```

---

## 2. `packages/ui-shadcn` — Library UI Bersama

Library komponen React berbasis shadcn/ui yang dapat dipakai oleh semua app.

**Package name:** `@gmp/ui-shadcn`

### Penting: Integrasi Inertia.js

Semua navigasi di dalam library ini menggunakan **Inertia.js**, bukan TanStack Router. App yang menggunakan library ini wajib memiliki Inertia.js sebagai dependency.

---

### Komponen Layout

#### `AuthenticatedLayout`

Layout utama untuk halaman yang memerlukan autentikasi. Sudah termasuk sidebar, header, dan sign-out dialog.

```tsx
// resources/js/pages/dashboard.tsx (di app Finance/Auth/dll)
import { AuthenticatedLayout } from '@gmp/ui-shadcn/components/layout/authenticated-layout'
import { router } from '@inertiajs/react'

export default function Dashboard() {
  function handleSignOut() {
    router.visit('/auth/logout', { method: 'post' })
  }

  return (
    <AuthenticatedLayout
      onSignOut={handleSignOut}   // wajib — dipassing ke sign-out dialog
      breadcrumbs={[
        { label: 'Dashboard', href: '/dashboard' }
      ]}
    >
      <h1>Selamat datang!</h1>
    </AuthenticatedLayout>
  )
}
```

**Prop `onSignOut`** bersifat framework-agnostic — kamu bisa isi dengan apapun:

```ts
// SSO logout (redirect ke auth app)
onSignOut={() => window.location.href = 'http://auth.gmp.local/logout'}

// Inertia POST request
onSignOut={() => router.visit('/logout', { method: 'post' })}

// Axios + redirect manual
onSignOut={async () => {
  await axios.post('/api/logout')
  window.location.href = '/login'
}}
```

---

#### `SignOutDialog`

Dialog konfirmasi sebelum sign out. Biasanya sudah terintegrasi dalam `AuthenticatedLayout`, tapi bisa dipakai standalone.

```tsx
import { SignOutDialog } from '@gmp/ui-shadcn/components/dialogs/sign-out-dialog'

<SignOutDialog
  open={isOpen}
  onOpenChange={setIsOpen}
  onConfirm={() => router.visit('/logout', { method: 'post' })}
/>
```

---

### Navigasi (Inertia.js)

Semua komponen navigasi menggunakan `Link` dan `router` dari `@inertiajs/react`:

```tsx
// nav-user, nav-group, top-nav — semua pakai Link dari inertia
import { Link } from '@inertiajs/react'

// Navigasi programmatic
import { router } from '@inertiajs/react'
router.visit('/users')

// Deteksi halaman aktif
import { usePage } from '@inertiajs/react'
const { url } = usePage()
const isActive = url.startsWith('/dashboard')
```

---

## 3. `packages/laravel-core` — Column Mapping System

Sistem pemetaan nama kolom database yang memungkinkan kamu menggunakan "alias" di seluruh kode, sehingga ketika nama kolom di DB berubah, **cukup update satu tempat**.

**Composer package:** `gmp/laravel-core`

---

### Konsep Dasar

Tanpa column mapping, kamu menulis nama kolom DB langsung di mana-mana:

```php
// ❌ Tanpa column mapping — nama kolom muncul di banyak tempat
User::where('usr_email', $email)->first();
User::orderBy('usr_nama_lengkap')->get();
'unique:auth_m_users,usr_email'
```

Jika kolom `usr_email` diganti menjadi `email`, kamu harus mengubah **semua** file tersebut.

Dengan column mapping:

```php
// ✅ Dengan column mapping — nama alias dipakai di mana-mana
User::where('email', $email)->first();   // auto-resolve ke usr_email
User::orderBy('name')->get();            // auto-resolve ke usr_nama_lengkap
'unique:auth_m_users,' . User::col('email')
```

Jika nama kolom DB berubah → cukup update `columnMap()` di model.

---

### Setup: Menambahkan `HasColumnMap` ke Model

```php
<?php

namespace App\Modules\Auth\Domain\Models;

use Gmp\LaravelCore\Traits\HasColumnMap;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasColumnMap;

    /**
     * Peta alias → nama kolom DB aktual.
     * Key   = alias yang dipakai di kode PHP
     * Value = nama kolom asli di tabel database
     */
    public static function columnMap(): array
    {
        return [
            'id'                 => 'id',
            'uuid'               => 'uuid',
            'name'               => 'usr_nama_lengkap',   // contoh kolom dengan prefix
            'email'              => 'usr_email',
            'email_verified_at'  => 'usr_email_verified',
            'status'             => 'usr_status',
            'role'               => 'usr_role',
            'created_at'         => 'created_at',
            'updated_at'         => 'updated_at',
        ];
    }
}
```

> **Catatan:** Kolom yang tidak ada di `columnMap()` tetap bisa digunakan normal (passthrough). Tidak semua kolom harus didaftarkan.

---

### `MappedBuilder` — Auto-resolve di Setiap Query

Setelah `HasColumnMap` di-use, **semua query Eloquent standar** secara otomatis menerjemahkan alias:

#### `where` / `orWhere`

```php
// Kamu tulis:
User::where('email', 'john@example.com')->first();

// SQL yang dikirim ke DB:
// SELECT * FROM auth_m_users WHERE usr_email = 'john@example.com'

// Dengan operator
User::where('name', 'LIKE', 'John%')->get();
// WHERE usr_nama_lengkap LIKE 'John%'

// orWhere
User::where('status', 'active')
    ->orWhere('role', 'admin')
    ->get();
// WHERE usr_status = 'active' OR usr_role = 'admin'
```

#### `whereIn` / `whereNotIn`

```php
User::whereIn('status', ['active', 'pending'])->get();
// WHERE usr_status IN ('active', 'pending')

User::whereNotIn('role', ['banned'])->get();
// WHERE usr_role NOT IN ('banned')
```

#### `whereNull` / `whereNotNull`

```php
User::whereNull('email_verified_at')->get();
// WHERE usr_email_verified IS NULL

User::whereNotNull('email_verified_at')->get();
// WHERE usr_email_verified IS NOT NULL
```

#### `whereBetween`

```php
User::whereBetween('created_at', ['2025-01-01', '2025-12-31'])->get();
// WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31'
```

#### `orderBy` / `orderByDesc`

```php
User::orderBy('name')->get();
// ORDER BY usr_nama_lengkap ASC

User::orderBy('created_at', 'desc')->get();
// ORDER BY created_at DESC

User::orderByDesc('name')->get();
// ORDER BY usr_nama_lengkap DESC
```

#### `select` / `addSelect`

```php
User::select(['name', 'email', 'status'])->get();
// SELECT usr_nama_lengkap, usr_email, usr_status

User::select('name as display_name')->get();
// SELECT usr_nama_lengkap as display_name
```

#### `groupBy` / `having`

```php
User::select('role')
    ->groupBy('role')
    ->having('role', '!=', 'banned')
    ->get();
// GROUP BY usr_role HAVING usr_role != 'banned'
```

#### `latest` / `oldest`

```php
User::latest('created_at')->get();
// ORDER BY created_at DESC

User::oldest('created_at')->get();
// ORDER BY created_at ASC
```

#### Agregasi: `sum`, `avg`, `min`, `max`, `pluck`, `value`

```php
User::sum('id');         // SUM(id)
User::avg('id');         // AVG(id)
User::pluck('name');     // ambil kolom usr_nama_lengkap sebagai array
User::value('email');    // ambil satu nilai dari usr_email
```

#### Format `table.column` (aman untuk JOIN)

```php
// MappedBuilder hanya resolve bagian setelah titik
User::join('roles', 'users.role', '=', 'roles.id')
    ->where('users.status', 'active')
    ->get();
// WHERE users.usr_status = 'active'
```

---

### Static Helpers (untuk dipakai di luar query)

Helpers ini dipakai di tempat yang tidak bisa menggunakan MappedBuilder, seperti validation rules.

#### `col(string $alias): string`

Resolve satu alias ke nama kolom DB.

```php
User::col('email')   // → 'usr_email'
User::col('name')    // → 'usr_nama_lengkap'
User::col('unknown') // → 'unknown' (passthrough jika tidak ada di map)
```

#### `colQualified(string $alias): string`

Resolve alias ke format `table.column`, berguna untuk JOIN.

```php
User::colQualified('email')  // → 'auth_m_users.usr_email'
User::colQualified('name')   // → 'auth_m_users.usr_nama_lengkap'

// Contoh di raw query JOIN
DB::table('orders')
  ->join('users', 'orders.user_id', '=', User::colQualified('id'))
  ->get();
```

#### `cols(array $aliases): array`

Resolve banyak alias sekaligus.

```php
User::cols(['name', 'email', 'status'])
// → ['usr_nama_lengkap', 'usr_email', 'usr_status']

// Contoh untuk DB::select manual
$columns = implode(', ', User::cols(['name', 'email']));
DB::select("SELECT {$columns} FROM auth_m_users");
```

#### `selectMapped(array $aliases = []): array`

Bangun array `db_column as alias` — berguna agar hasil query bisa diakses dengan nama alias.

```php
// Semua kolom dalam map
User::selectMapped()
// → ['id as id', 'usr_email as email', 'usr_nama_lengkap as name', ...]

// Hanya sebagian
User::selectMapped(['name', 'email'])
// → ['usr_nama_lengkap as name', 'usr_email as email']

// Contoh penggunaan
$users = User::select(User::selectMapped(['name', 'email']))->get();
// Kolom di hasil → $user->name, $user->email (bukan usr_nama_lengkap)
```

---

### Scope: `withMappedColumns`

Shortcut untuk `select(User::selectMapped(...))`.

```php
// Semua kolom dalam map
User::withMappedColumns()->get();

// Hanya sebagian
User::withMappedColumns(['name', 'email', 'status'])->get();
```

---

### Penggunaan di Form Request (Validation)

Ini adalah tempat utama di mana static helper diperlukan karena string validation rule tidak melalui MappedBuilder.

```php
<?php

namespace App\Modules\Auth\Infrastructure\Requests;

use App\Modules\Auth\Domain\Models\User;
use Illuminate\Foundation\Http\FormRequest;

class CreateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            // Tanpa helper: 'unique:auth_m_users,usr_email' ← hardcoded
            // Dengan helper:
            'email'    => [
                'required',
                'email',
                'unique:' . (new User)->getTable() . ',' . User::col('email'),
                //          → auth_m_users                    → usr_email
            ],
            'password' => ['required', 'min:8'],
        ];
    }
}

class UpdateUserRequest extends FormRequest
{
    public function rules(): array
    {
        $userId = $this->route('user')?->id;

        return [
            'email' => [
                'required',
                'email',
                // Exclude current user dari unique check
                'unique:' . (new User)->getTable() . ',' . User::col('email') . ',' . $userId,
            ],
        ];
    }
}
```

---

### Penggunaan di Repository

Repository cukup menggunakan Eloquent standar — `MappedBuilder` bekerja di balik layar:

```php
<?php

namespace App\Modules\Auth\Infrastructure\Repositories;

use App\Modules\Auth\Domain\Models\User;

class EloquentUserRepository
{
    public function __construct(private readonly User $model) {}

    // Query standar — MappedBuilder auto-resolve alias
    public function findByUuid(string $uuid): ?User
    {
        return $this->model->where('uuid', $uuid)->first();
        // SQL: WHERE uuid = ?  (uuid tidak di-map, passthrough)
    }

    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
        // SQL: WHERE usr_email = ?
    }

    public function findActive(int $perPage = 20): LengthAwarePaginator
    {
        return $this->model
            ->where('status', 'active')   // → WHERE usr_status = 'active'
            ->orderBy('name')             // → ORDER BY usr_nama_lengkap
            ->paginate($perPage);
    }

    public function search(string $keyword): Collection
    {
        return $this->model
            ->where('name', 'LIKE', "%{$keyword}%")   // → WHERE usr_nama_lengkap LIKE ...
            ->orWhere('email', 'LIKE', "%{$keyword}%") // → OR usr_email LIKE ...
            ->orderBy('name')
            ->get();
    }
}
```

---

### Skenario: Rename Kolom DB

Asumsikan kolom `usr_email` di DB diganti menjadi `email` saja. Yang perlu diubah **hanya satu baris**:

```php
// Sebelum
public static function columnMap(): array
{
    return [
        'email' => 'usr_email',   // ← ubah ini
    ];
}

// Sesudah
public static function columnMap(): array
{
    return [
        'email' => 'email',   // ✅ seluruh kodebase otomatis ikut
    ];
}
```

Tidak ada perubahan di repository, controller, request, atau test.

---

### Catatan Penting

| Situasi | Cara handle |
|---------|-------------|
| Kolom tidak ada di `columnMap()` | Passthrough otomatis — tetap bisa dipakai |
| Raw expression (`DB::raw(...)`) | Passthrough — `MappedBuilder` tidak menyentuh Expression |
| String validation rule (`unique:table,col`) | Gunakan `User::col()` secara manual |
| Query ke tabel lain (JOIN, subquery) | Gunakan nama kolom DB asli atau `Model::colQualified()` |
| `whereColumn('col1', 'col2')` | Kedua kolom di-resolve via `columnMap()` |

---

## 4. Tooling & Config

### `pint.json` — Root PHP Code Style

Konfigurasi PHP CS Fixer (via Laravel Pint) disimpan di root monorepo, dipakai oleh semua app.

```bash
# Dari root
./vendor/bin/pint

# Atau via composer di masing-masing app (sudah terkonfigurasi)
cd apps/auth && composer lint

# Dry-run (hanya tampilkan, tidak ubah file)
./vendor/bin/pint --test
```

File: [`pint.json`](pint.json) di root mengatur preset, rules, dan path.

---

### npm Workspaces — `@gmp/*` Packages

Semua package TypeScript terhubung via npm workspaces. Setelah `npm install` di root, alias berikut tersedia di setiap app:

| Import | Package |
|--------|---------|
| `@gmp/ui-shadcn` | `packages/ui-shadcn` |
| `@gmp/utils` | `packages/utils` |
| `@gmp/types` | `packages/types` |

```bash
# Install / update dependencies dari root
npm install

# Menambah dependency ke package tertentu
npm install some-lib --workspace=packages/utils
```

---

### `tsconfig.json` Path Aliases

Setiap app memiliki path alias `@gmp/utils` di `tsconfig.json` agar TypeScript tidak complain:

```json
// apps/auth/tsconfig.json (dan finance, dll)
{
  "compilerOptions": {
    "paths": {
      "@gmp/utils": ["../../packages/utils/index.ts"],
      "@gmp/ui-shadcn/*": ["../../packages/ui-shadcn/*"]
    }
  }
}
```

---

*Dokumen ini diperbarui pada: 26 Maret 2026*
