# Kontrak API, CRM Kolektor

Dokumen ini adalah kesepakatan antara **tim backend** (`server/`) dan **tim
frontend** (`client/`). Frontend **hanya** boleh berkomunikasi lewat endpoint di
bawah ini; tidak ada akses langsung ke file data.

Aturan umum:
- Semua response JSON. Error berbentuk `{ "error": "<pesan>" }` dengan kode HTTP.
- Semua endpoint kecuali `POST /api/auth/login` memerlukan cookie sesi
  `crm_session` (httpOnly, SameSite=Lax, umur 7 hari). Belum login → `401`.
- Otorisasi per peran ditegakkan di backend. Peran: `admin` (Admin Collection),
  `spv`, `tl`, `hr`, `agent`. SPV dibatasi ke tim pada `user.teams`;
  admin & HR berlaku lintas tim; agent hanya melihat datanya sendiri.
  **TL punya full akses pembayaran agent timnya sendiri** (lihat riwayat, input,
  revisi, hapus) plus dashboard keseluruhan: `/api/state` untuk TL mengirim
  transaksi timnya (`payments`) + agregat global `paidByAgent`/`paidByDay`,
  tanpa transaksi tim lain. `/api/agents`, `/api/teams`, `/api/users`, dan
  endpoint ekspor tetap ditolak (`403`).
- Setiap tim punya `side: 'front' | 'back'` (sisi FRONT/BACK dari tab sheet
  asalnya), dipakai untuk badge warna dan pengelompokan di seluruh tampilan.

## Auth

| Method | Path | Peran | Catatan |
|---|---|---|---|
| POST | `/api/auth/login` | - | Body `{username, password}` → `{user}` + Set-Cookie |
| POST | `/api/auth/logout` | semua | Mencabut sesi |
| GET | `/api/auth/me` | semua | `{user}` sesi aktif |
| POST | `/api/auth/change-password` | semua | Body `{oldPassword, newPassword}` |

## State (payload utama)

| Method | Path | Peran | Catatan |
|---|---|---|---|
| GET | `/api/state` | semua | `{period, user, teams, agents, payments}`. **Untuk agent**: hanya berisi `{me: {agent, team}}` + data miliknya. |

## Pengguna (admin & HR; HR tidak bisa menyentuh akun admin)

| Method | Path | Catatan |
|---|---|---|
| GET | `/api/users` | `{users}` tanpa hash password |
| POST | `/api/users` | Body `{username, name, role, password, teams?, agentId?}`. Username unik `^[a-z0-9._-]{3,32}$`, password min 6 |
| PUT | `/api/users/:id` | Body parsial `{name?, role?, teams?, agentId?, active?}` |
| POST | `/api/users/:id/password` | Body `{password}` (reset; sesi target dicabut) |
| DELETE | `/api/users/:id` | Tidak boleh diri sendiri |
| GET | `/api/meta/roles` | Daftar peran |

## Agent, baca: semua kecuali agent; tulis: admin & SPV (dalam lingkup tim)

| Method | Path | Catatan |
|---|---|---|
| GET | `/api/agents` | `{agents}`, tiap agent punya `resignDate` (ISO atau kosong) |
| GET | `/api/agents/:id/audit` | `{audit}` jejak revisi pembayaran agent (terbaru dulu, maks. 50). Agent hanya miliknya; SPV/TL hanya lingkup timnya. Entri: `{ts, userName, role, action: 'input'\|'revise'\|'delete', date, oldAmount, newAmount}` |
| POST | `/api/agents` | `{employeeId?, name, account?, teamId, targetHarian?, targetUtama?, startDate?, endDate?, resignDate?, osBaru?}` |
| PUT | `/api/agents/:id` | Body parsial; pindah tim harus tetap dalam lingkup, sertakan `transferDate` (YYYY-MM-DD) sebagai tanggal efektif — perolehan sebelum tanggal itu tetap milik tim lama (riwayat `teamHistory` dicatat otomatis). `resignDate` mengaktifkan prorata target; `deactivateUser: true` ikut menonaktifkan akun login agent terkait |
| DELETE | `/api/agents/:id` | Menghapus bersama riwayat pembayaran; lepas penautan akun agent |

**Prorata resign**: target bulanan efektif = `targetHarian × hari kerja efektif`
(tanggal resign dihitung masuk; resign sebelum periode → target 0; setelah
periode/kosong → target penuh). Frontend menghitung hal yang sama lewat
`effAgent()` untuk semua KPI/rate/ranking.

## Tim, baca: semua kecuali agent; tambah/hapus: admin; ubah: admin & SPV (timnya)

| Method | Path | Catatan |
|---|---|---|
| GET | `/api/teams` | `{teams}` (ada `side`, `targetPct`, `insentifThreshold`) |
| POST | `/api/teams` | `{name, side?, supervisor?, incentiveRate?}`, `side` default `'back'` |
| PUT | `/api/teams/:id` | Body parsial (termasuk `side`) |
| DELETE | `/api/teams/:id` | Ditolak `409` bila masih ada agent |

## Pembayaran, input admin/SPV/TL (dalam lingkup tim); agent hanya baca

| Method | Path | Catatan |
|---|---|---|
| GET | `/api/payments` | Agent menerima miliknya; **TL hanya menerima transaksi timnya sendiri**; admin/SPV/HR menerima semua |
| GET | `/api/payments/paid-ids?date=YYYY-MM-DD` | `{agentIds}`, id agent yang sudah membayar pada tanggal itu (tanpa nominal), ter-scope per peran; dipakai fitur "Belum Input" |
| POST | `/api/payments` | `{agentId, date: "YYYY-MM-DD", amount}`. **Upsert**: satu entri per agent per tanggal. Setiap mutasi (input/revisi/hapus) tercatat ke jejak audit |
| PUT | `/api/payments/:id` | `{amount}` |
| DELETE | `/api/payments/:id` | |

## Ekspor, admin/SPV/TL/HR

| Method | Path | Isi |
|---|---|---|
| GET | `/api/export/rekap.csv` | Rekap per agent (target, realisasi, rate, insentif) |
| GET | `/api/export/payments.csv` | Semua transaksi pembayaran |

## Konvensi

- Uang: angka (rupiah penuh, tanpa desimal). Tanggal: `YYYY-MM-DD` waktu lokal.
- Rate insentif: pecahan (0.013825 = 1,3825%).
- Nama field konsisten dengan seed: `period` (`"YYYY-MM"`), `teamId`, `agentId`.
- Penyimpanan: `server/data/crm-data.json` (write atomic via rename).
- Password di-hash scrypt (N=4096) dengan salt per user, tidak pernah dikirim ke client.

### OS Harian (target dinamis FRONT)

| Method | Path | Keterangan |
|---|---|---|
| GET | `/api/os` | `{os}` entri OS harian (ter-scope: agent hanya miliknya, TL hanya timnya) |
| POST | `/api/os` | `{agentId, date: "YYYY-MM-DD", os}` upsert per agent per tanggal. Hak input: admin/SPV (lingkup), TL khusus tim FRONT-nya, agent FRONT untuk OS dirinya |
| DELETE | `/api/os/:id` | Hapus entri OS |

### Insentif (skema ambang)

`insentif = 1.500.000 × tingkatPenarikan × (hariKerja / hariBulan)` bila rate
≥ ambang tim (`insentifThreshold`, default FRONT 0.8 / BACK 0.5); selain itu 0.

### Absensi

| Method | Path | Keterangan |
|---|---|---|
| GET | `/api/absensi` | `{absensi}` semua entri (agent hanya miliknya, TL hanya timnya saat ini). Query `?date=YYYY-MM-DD` opsional |
| POST | `/api/absensi` | `{agentId, date, status, note?}` upsert per agent per tanggal. Status: `hadir`/`izin`/`sakit`/`alpha`/`cuti`. Hak input: TL (timnya), SPV (lingkup), admin. HR hanya membaca |
| DELETE | `/api/absensi/:id` | Hapus entri (TL/SPV/admin dalam lingkup) |
