# API Documentation - Absensi & Penggajian

> **Base URL:** `https://your-domain.com/api/v1`
>
> **Authentication:** Bearer Token (Laravel Sanctum)
>
> **Content-Type:** `application/json` (kecuali endpoint upload yang pakai `multipart/form-data`)

---

## Response Format (Standard)

Semua endpoint mengembalikan format JSON yang konsisten:

```json
{
  "success": true,
  "message": "Pesan deskriptif",
  "data": { ... }
}
```

**HTTP Status Codes:**
| Code | Keterangan |
|------|-----------|
| 200 | Sukses |
| 400 | Bad Request (validasi bisnis gagal) |
| 401 | Unauthorized (token salah/expired) |
| 403 | Forbidden (akun tidak aktif) |
| 404 | Not Found |
| 422 | Validation Error |

---

## 1. AUTH

### 1.1 Login

| | |
|---|---|
| **Method** | `POST` |
| **URL** | `/login` |
| **Auth** | Tidak perlu |
| **Content-Type** | `application/json` |

**Request Body:**
```json
{
  "username": "marko",
  "password": "password123"
}
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "Login berhasil",
  "data": {
    "user": {
      "id": 4,
      "nama_lengkap": "Marko",
      "username": "marko",
      "email": "marko@email.com",
      "nik": "16100213213",
      "jabatan": "Staff IT",
      "departemen": "Teknologi",
      "no_telepon": "081234567890",
      "alamat": "Jl. Contoh No. 1",
      "tipe_gaji": "harian",
      "nominal_gaji": 250000.00,
      "tanggal_bergabung": "2025-01-01",
      "role": "pegawai",
      "foto": "https://your-domain.com/storage/foto-profil/abc.jpg",
      "shift": {
        "id": 1,
        "nama": "Shift Pagi",
        "jam_masuk": "08:00:00",
        "jam_pulang": "17:00:00",
        "toleransi_terlambat": 15
      },
      "company": {
        "id": 1,
        "nama": "Erdius Brilink",
        "logo": "https://your-domain.com/storage/logo/company.png"
      },
      "outlet": {
        "id": 1,
        "nama": "Outlet Pusat",
        "kode": "PUSAT",
        "alamat": "Jl. Contoh No.123"
      }
    },
    "token": "1|abc123def456..."
  }
}
```

**Error Responses:**

*401 - Salah password:*
```json
{
  "success": false,
  "message": "Username atau password salah",
  "data": null
}
```

*403 - Akun nonaktif:*
```json
{
  "success": false,
  "message": "Akun Anda tidak aktif. Hubungi admin.",
  "data": null
}
```

> **Catatan Flutter:** Simpan `token` ke secure storage. Gunakan di setiap request selanjutnya sebagai Header `Authorization: Bearer {token}`.

---

### 1.2 Logout

| | |
|---|---|
| **Method** | `POST` |
| **URL** | `/logout` |
| **Auth** | Bearer Token |

**Request Header:**
```
Authorization: Bearer {token}
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "Logout berhasil",
  "data": null
}
```

> **Catatan Flutter:** Hapus token dari secure storage setelah logout berhasil.

---

## 2. PROFIL

### 2.1 Get Profile

| | |
|---|---|
| **Method** | `GET` |
| **URL** | `/profile` |
| **Auth** | Bearer Token |

**Success Response (200):**
```json
{
  "success": true,
  "message": "Data profil",
  "data": {
    "id": 4,
    "nama_lengkap": "Marko",
    "username": "marko",
    "email": "marko@email.com",
    "nik": "16100213213",
    "jabatan": "Staff IT",
    "departemen": "Teknologi",
    "no_telepon": "081234567890",
    "alamat": "Jl. Contoh No. 1",
    "tipe_gaji": "harian",
    "nominal_gaji": 250000.00,
    "tanggal_bergabung": "2025-01-01",
    "role": "pegawai",
    "foto": "https://your-domain.com/storage/foto-profil/abc.jpg",
    "shift": {
      "id": 1,
      "nama": "Shift Pagi",
      "jam_masuk": "08:00:00",
      "jam_pulang": "17:00:00",
      "toleransi_terlambat": 15
    },
    "company": {
      "id": 1,
      "nama": "Erdius Brilink",
      "logo": "https://your-domain.com/storage/logo/company.png"
    },
    "outlet": {
      "id": 1,
      "nama": "Outlet Pusat",
      "kode": "PUSAT",
      "alamat": "Jl. Contoh No.123"
    }
  }
}
```

---

### 2.2 Update Profile

| | |
|---|---|
| **Method** | `POST` |
| **URL** | `/profile/update` |
| **Auth** | Bearer Token |
| **Content-Type** | `multipart/form-data` |

**Request Body (semua opsional):**

| Field | Type | Rules | Keterangan |
|-------|------|-------|------------|
| `nama_lengkap` | string | max 255 | Nama tampilan |
| `email` | string | email, unique | Email baru |
| `no_telepon` | string | max 20 | Nomor HP |
| `alamat` | string | max 500 | Alamat rumah |
| `foto` | file | image, max 2MB | Foto profil (jpg/png) |

**Request Example (multipart/form-data):**
```
nama_lengkap: Marko Baru
email: markobaru@email.com
no_telepon: 082112345678
foto: [FILE]
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "Profil berhasil diperbarui",
  "data": {
    "id": 4,
    "nama_lengkap": "Marko Baru",
    "...": "...sama seperti GET /profile"
  }
}
```

---

### 2.3 Change Password

| | |
|---|---|
| **Method** | `POST` |
| **URL** | `/profile/change-password` |
| **Auth** | Bearer Token |
| **Content-Type** | `application/json` |

**Request Body:**
```json
{
  "current_password": "password123",
  "new_password": "newpass456",
  "new_password_confirmation": "newpass456"
}
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "Password berhasil diubah",
  "data": null
}
```

**Error 400:**
```json
{
  "success": false,
  "message": "Password lama tidak sesuai",
  "data": null
}
```

---

## 3. ABSENSI

### 3.1 Cek Status Hari Ini

> **Panggil ini saat buka halaman absen.** Endpoint ini mengembalikan status absensi hari ini + pengaturan radius & shift (untuk map di Flutter).

| | |
|---|---|
| **Method** | `GET` |
| **URL** | `/absen/hari-ini` |
| **Auth** | Bearer Token |

**Success Response (200):**
```json
{
  "success": true,
  "message": "Status absensi hari ini",
  "data": {
    "sudah_absen_masuk": false,
    "sudah_absen_pulang": false,
    "absensi": null,
    "pengaturan": {
      "shift": {
        "id": 1,
        "nama": "Shift Pagi",
        "jam_masuk": "08:00:00",
        "jam_pulang": "17:00:00",
        "toleransi": 15
      },
      "lat_kantor": "-6.2088",
      "lng_kantor": "106.8456",
      "radius": "100"
    }
  }
}
```

**Jika sudah absen masuk:**
```json
{
  "success": true,
  "message": "Status absensi hari ini",
  "data": {
    "sudah_absen_masuk": true,
    "sudah_absen_pulang": false,
    "absensi": {
      "tanggal": "2026-02-28",
      "jam_masuk": "07:55:00",
      "jam_pulang": null,
      "status": "hadir",
      "keterangan": null
    },
    "pengaturan": { "...": "..." }
  }
}
```

> **Catatan Flutter:**
> - Gunakan `lat_kantor`, `lng_kantor`, `radius` untuk menampilkan peta & circle radius
> - Gunakan `sudah_absen_masuk` / `sudah_absen_pulang` untuk menentukan tombol mana yang aktif
> - `shift.toleransi` = menit toleransi terlambat. Bisa tampilkan countdown jika mau

---

### 3.2 Absen Masuk

| | |
|---|---|
| **Method** | `POST` |
| **URL** | `/absen/masuk` |
| **Auth** | Bearer Token |
| **Content-Type** | `multipart/form-data` |

**Request Body:**

| Field | Type | Required | Keterangan |
|-------|------|----------|------------|
| `lat` | double | ✅ | Latitude GPS pegawai |
| `lng` | double | ✅ | Longitude GPS pegawai |
| `foto` | file | ✅ | Foto selfie (jpg/png, max 5MB) |

**Request Example:**
```
lat: -6.208800
lng: 106.845600
foto: [FILE dari kamera]
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "Absen masuk berhasil. Status: Hadir",
  "data": {
    "absensi_id": 25,
    "jam_masuk": "07:55:00",
    "status": "hadir"
  }
}
```

**Status otomatis:**
- `hadir` = jam masuk ≤ shift.jam_masuk + toleransi
- `terlambat` = jam masuk > shift.jam_masuk + toleransi

**Error Responses:**

*400 - Sudah absen:*
```json
{
  "success": false,
  "message": "Anda sudah absen masuk hari ini",
  "data": null
}
```

*400 - Di luar radius:*
```json
{
  "success": false,
  "message": "Anda berada di luar radius kantor. Tidak bisa absen.",
  "data": null
}
```

> **Catatan Flutter:**
> - Ambil GPS dari `geolocator` package
> - Foto dari kamera depan (selfie) pakai `image_picker`
> - Kirim sebagai `multipart/form-data` (bukan base64)
> - Cek radius di sisi Flutter juga (untuk UX), tapi server tetap validasi ulang
> - Tampilkan status hadir/terlambat dari response

---

### 3.3 Absen Pulang

| | |
|---|---|
| **Method** | `POST` |
| **URL** | `/absen/pulang` |
| **Auth** | Bearer Token |
| **Content-Type** | `multipart/form-data` |

**Request Body:**

| Field | Type | Required | Keterangan |
|-------|------|----------|------------|
| `lat` | double | ✅ | Latitude GPS pegawai |
| `lng` | double | ✅ | Longitude GPS pegawai |
| `foto` | file | ✅ | Foto selfie (jpg/png, max 5MB) |

**Success Response (200):**
```json
{
  "success": true,
  "message": "Absen pulang berhasil",
  "data": {
    "absensi_id": 25,
    "jam_masuk": "07:55:00",
    "jam_pulang": "17:05:00",
    "status": "hadir"
  }
}
```

**Error:**
```json
{
  "success": false,
  "message": "Anda belum absen masuk hari ini",
  "data": null
}
```

---

### 3.4 Riwayat Absensi

| | |
|---|---|
| **Method** | `GET` |
| **URL** | `/absen/riwayat` |
| **Auth** | Bearer Token |

**Query Parameters (opsional):**

| Param | Type | Default | Keterangan |
|-------|------|---------|------------|
| `bulan` | int | bulan sekarang | 1-12 |
| `tahun` | int | tahun sekarang | 2024-2030 |

**Request Example:**
```
GET /api/v1/absen/riwayat?bulan=2&tahun=2026
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "Riwayat absensi",
  "data": {
    "bulan": 2,
    "tahun": 2026,
    "summary": {
      "hadir": 16,
      "terlambat": 4,
      "izin": 0,
      "alpha": 0
    },
    "data": [
      {
        "id": 21,
        "tanggal": "2026-02-28",
        "jam_masuk": "07:55:00",
        "jam_pulang": "17:05:00",
        "status": "hadir",
        "keterangan": null
      },
      {
        "id": 20,
        "tanggal": "2026-02-27",
        "jam_masuk": "08:25:00",
        "jam_pulang": "17:00:00",
        "status": "terlambat",
        "keterangan": null
      }
    ]
  }
}
```

> **Catatan Flutter:**
> - Tampilkan `summary` sebagai card statistik di atas (dengan warna: hadir=hijau, terlambat=orange, izin=biru, alpha=merah)
> - Data diurutkan dari terbaru
> - Status values: `hadir`, `terlambat`, `izin`, `alpha`
> - Bisa tambahkan filter bulan/tahun picker di UI

---

## 4. GAJI (PENGGAJIAN)

### 4.1 Riwayat Gaji

| | |
|---|---|
| **Method** | `GET` |
| **URL** | `/gaji/riwayat` |
| **Auth** | Bearer Token |

**Success Response (200):**
```json
{
  "success": true,
  "message": "Riwayat gaji",
  "data": {
    "data": [
      {
        "id": 7,
        "periode": "01/02/2026 - 12/02/2026",
        "periode_mulai": "2026-02-01",
        "periode_selesai": "2026-02-12",
        "tipe_periode": "harian",
        "nominal_gaji": 250000.00,
        "gaji_pokok": 2250000.00,
        "total_insentif": 0.00,
        "total_potongan": 3000.00,
        "gaji_bersih": 2247000.00,
        "total_hari_kerja": 10,
        "total_hadir": 9,
        "total_terlambat": 2,
        "total_alpha": 0,
        "status": "sudah_bayar",
        "tanggal_bayar": "28/02/2026 07:33"
      }
    ]
  }
}
```

**Field Reference:**

| Field | Type | Keterangan |
|-------|------|------------|
| `tipe_periode` | string | `harian` atau `bulanan` |
| `nominal_gaji` | float | Gaji per hari (harian) atau gaji pokok (bulanan) |
| `gaji_pokok` | float | Total gaji pokok (harian: nominal × hadir, bulanan: nominal) |
| `status` | string | `belum_bayar` atau `sudah_bayar` |

> **Catatan Flutter:**
> - Tampilkan sebagai list card
> - Warna status: `sudah_bayar` = hijau, `belum_bayar` = merah
> - Harian: tampilkan "Rp 250.000/hari × 9 hari = Rp 2.250.000"
> - Klik card untuk buka detail slip

---

### 4.2 Detail Slip Gaji

| | |
|---|---|
| **Method** | `GET` |
| **URL** | `/gaji/slip/{id}` |
| **Auth** | Bearer Token |

**Success Response (200):**
```json
{
  "success": true,
  "message": "Detail slip gaji",
  "data": {
    "id": 7,
    "periode": "01/02/2026 - 12/02/2026",
    "periode_mulai": "2026-02-01",
    "periode_selesai": "2026-02-12",
    "tipe_periode": "harian",
    "total_hari_kerja": 10,
    "total_hadir": 9,
    "total_terlambat": 2,
    "total_alpha": 0,
    "nominal_gaji": 250000.00,
    "tipe_gaji": "harian",
    "gaji_pokok": 2250000.00,
    "insentifs": [
      {
        "nama": "Bonus Kehadiran",
        "nominal": 50000.00,
        "keterangan": "Full hadir minggu ini"
      }
    ],
    "potongans": [
      {
        "nama": "Potongan telat",
        "nominal": 3000.00,
        "keterangan": "Terlambat (2x)"
      }
    ],
    "total_insentif": 50000.00,
    "total_potongan": 3000.00,
    "gaji_bersih": 2297000.00,
    "status": "sudah_bayar",
    "dibayar_oleh": "Muhammad ari erdius",
    "tanggal_bayar": "28/02/2026 07:33"
  }
}
```

**Error 404:**
```json
{
  "success": false,
  "message": "Slip gaji tidak ditemukan",
  "data": null
}
```

> **Catatan Flutter:**
> - Buat layout slip mirip struk/receipt
> - Section 1: Info pegawai & kehadiran
> - Section 2: Pendapatan (gaji pokok + insentifs)
> - Section 3: Potongan (list potongans)
> - Section 4: Total gaji bersih (highlight besar)
> - Gunakan `tipe_gaji` untuk conditional layout:
>   - Harian: "Rp {nominal_gaji}/hari × {total_hadir} hari"
>   - Bulanan: "Gaji Bulanan (Masuk {total_hadir}/{total_hari_kerja} hari)"

---

### 4.3 Download Slip Gaji PDF

| | |
|---|---|
| **Method** | `GET` |
| **URL** | `/gaji/slip/{id}/download` |
| **Auth** | Bearer Token |
| **Response** | File PDF (binary) |

**Request Example:**
```
GET /api/v1/gaji/slip/7/download
Authorization: Bearer {token}
```

**Success Response:**
- **Status:** 200
- **Content-Type:** `application/pdf`
- **Content-Disposition:** `attachment; filename="slip-gaji-marko-20260201.pdf"`
- **Body:** Binary PDF file

**Error 404:** JSON response biasa.

> **Catatan Flutter:**
> ```dart
> // Contoh download PDF di Flutter
> import 'package:dio/dio.dart';
> import 'package:path_provider/path_provider.dart';
> import 'package:open_file/open_file.dart';
>
> Future<void> downloadSlipPdf(int slipId, String token) async {
>   final dir = await getApplicationDocumentsDirectory();
>   final filePath = '${dir.path}/slip-gaji-$slipId.pdf';
>
>   await Dio().download(
>     '$baseUrl/gaji/slip/$slipId/download',
>     filePath,
>     options: Options(headers: {'Authorization': 'Bearer $token'}),
>   );
>
>   OpenFile.open(filePath); // buka PDF
> }
> ```

---

## RINGKASAN SEMUA ENDPOINT

| # | Method | Endpoint | Auth | Deskripsi |
|---|--------|----------|------|-----------|
| 1 | `POST` | `/login` | ❌ | Login, dapat token |
| 2 | `POST` | `/logout` | ✅ | Logout, hapus token |
| 3 | `GET` | `/profile` | ✅ | Data profil lengkap |
| 4 | `POST` | `/profile/update` | ✅ | Update profil + foto |
| 5 | `POST` | `/profile/change-password` | ✅ | Ganti password |
| 6 | `GET` | `/absen/hari-ini` | ✅ | Status absen hari ini + pengaturan radius |
| 7 | `POST` | `/absen/masuk` | ✅ | Absen masuk (GPS + foto) |
| 8 | `POST` | `/absen/pulang` | ✅ | Absen pulang (GPS + foto) |
| 9 | `GET` | `/absen/riwayat` | ✅ | Riwayat absensi per bulan |
| 10 | `GET` | `/gaji/riwayat` | ✅ | Daftar semua slip gaji |
| 11 | `GET` | `/gaji/slip/{id}` | ✅ | Detail slip gaji |
| 12 | `GET` | `/gaji/slip/{id}/download` | ✅ | Download PDF slip gaji |

---

## PANDUAN IMPLEMENTASI FLUTTER

### Setup Awal

```dart
// lib/core/constants.dart
class ApiConstants {
  static const String baseUrl = 'https://your-domain.com/api/v1';

  // Auth
  static const String login = '/login';
  static const String logout = '/logout';

  // Profile
  static const String profile = '/profile';
  static const String profileUpdate = '/profile/update';
  static const String changePassword = '/profile/change-password';

  // Absensi
  static const String absenHariIni = '/absen/hari-ini';
  static const String absenMasuk = '/absen/masuk';
  static const String absenPulang = '/absen/pulang';
  static const String riwayatAbsensi = '/absen/riwayat';

  // Gaji
  static const String riwayatGaji = '/gaji/riwayat';
  static String slipGaji(int id) => '/gaji/slip/$id';
  static String downloadSlip(int id) => '/gaji/slip/$id/download';
}
```

### Package yang Dibutuhkan

```yaml
# pubspec.yaml
dependencies:
  dio: ^5.0.0               # HTTP client
  flutter_secure_storage: ^9.0.0  # Simpan token
  geolocator: ^10.0.0       # GPS location
  image_picker: ^1.0.0      # Kamera selfie
  google_maps_flutter: ^2.5.0    # Map radius
  path_provider: ^2.1.0     # Download path
  open_file: ^3.3.0         # Buka PDF
  intl: ^0.18.0             # Format currency/date
```

### Struktur Halaman Flutter

```
lib/
├── core/
│   ├── constants.dart       # API URLs
│   ├── api_service.dart     # Dio wrapper + interceptor token
│   └── models/
│       ├── user_model.dart
│       ├── absensi_model.dart
│       ├── gaji_model.dart
│       └── slip_gaji_model.dart
├── features/
│   ├── auth/
│   │   ├── login_screen.dart
│   │   └── auth_provider.dart
│   ├── home/
│   │   └── home_screen.dart       # Dashboard utama
│   ├── absensi/
│   │   ├── absensi_screen.dart    # Map + tombol absen
│   │   └── riwayat_absensi_screen.dart
│   ├── gaji/
│   │   ├── riwayat_gaji_screen.dart
│   │   └── slip_gaji_screen.dart  # Detail + download PDF
│   └── profile/
│       ├── profile_screen.dart
│       ├── edit_profile_screen.dart
│       └── change_password_screen.dart
└── main.dart
```

### Flow Utama

```
Login Screen
    ↓ POST /login → simpan token
Home Screen (Dashboard)
    ├── GET /absen/hari-ini → tampilkan status
    ├── Tombol "Absen Masuk" → POST /absen/masuk
    ├── Tombol "Absen Pulang" → POST /absen/pulang
    ↓
Tab Navigation:
    ├── 🏠 Home (absen hari ini + peta radius)
    ├── 📋 Riwayat (GET /absen/riwayat)
    ├── 💰 Gaji (GET /gaji/riwayat → GET /gaji/slip/{id})
    └── 👤 Profil (GET /profile)
```

### Alur Absensi di Flutter

```
1. Buka halaman Absen
2. GET /absen/hari-ini → dapat lat_kantor, lng_kantor, radius
3. Tampilkan Google Map + circle radius
4. Ambil GPS pegawai via geolocator
5. Show marker lokasi pegawai di map
6. Hitung jarak (client-side) → enable/disable tombol
7. Klik "Absen Masuk":
   a. Buka kamera selfie (image_picker)
   b. Compress foto
   c. POST /absen/masuk {lat, lng, foto}
   d. Tampilkan response (hadir/terlambat)
8. Klik "Absen Pulang": sama seperti di atas tapi POST /absen/pulang
```

### Contoh API Service (Dio)

```dart
// lib/core/api_service.dart
class ApiService {
  late Dio _dio;
  final FlutterSecureStorage _storage = FlutterSecureStorage();

  ApiService() {
    _dio = Dio(BaseOptions(
      baseUrl: ApiConstants.baseUrl,
      connectTimeout: Duration(seconds: 30),
      receiveTimeout: Duration(seconds: 30),
    ));

    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await _storage.read(key: 'auth_token');
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        options.headers['Accept'] = 'application/json';
        return handler.next(options);
      },
      onError: (error, handler) {
        if (error.response?.statusCode == 401) {
          // Token expired → redirect ke login
          // navigatorKey.currentState?.pushReplacementNamed('/login');
        }
        return handler.next(error);
      },
    ));
  }

  // Login
  Future<Map<String, dynamic>> login(String username, String password) async {
    final response = await _dio.post('/login', data: {
      'username': username,
      'password': password,
    });
    if (response.data['success']) {
      await _storage.write(key: 'auth_token', value: response.data['data']['token']);
    }
    return response.data;
  }

  // Absen masuk
  Future<Map<String, dynamic>> absenMasuk(double lat, double lng, File foto) async {
    FormData formData = FormData.fromMap({
      'lat': lat,
      'lng': lng,
      'foto': await MultipartFile.fromFile(foto.path, filename: 'selfie.jpg'),
    });
    final response = await _dio.post('/absen/masuk', data: formData);
    return response.data;
  }
}
```

---

## CATATAN TEKNIS

1. **Token Storage**: Gunakan `flutter_secure_storage`, BUKAN `SharedPreferences`
2. **Foto Upload**: Compress dulu sebelum upload (max 5MB). Gunakan `flutter_image_compress`
3. **GPS Permission**: Minta izin lokasi di Android manifest & Info.plist
4. **Radius Check**: Server SELALU validasi radius (tidak bisa dibypass). Client-side check hanya untuk UX
5. **Status Absen Otomatis**: Server yang menentukan hadir/terlambat berdasarkan shift. Flutter hanya mengirim data
6. **Offline Handling**: Tampilkan pesan jika tidak ada internet. Tidak ada queue offline
7. **PDF Download**: Response bukan JSON tapi binary file. Handle di Dio dengan `responseType: ResponseType.bytes`
