# Analisis: Dual Service untuk Sub-Agenda (AHO vs Non-AHO)

> **Status:** Analisis & Rancangan — belum ada perubahan kode  
> **Branch:** `HRIS-202-aho`  
> **Tanggal:** 2026-04-13

---

## 1. Latar Belakang

User ingin form dan service sub-agenda dibedakan berdasarkan posisi karyawan:

| Tipe | Kondisi | Form | Validasi |
|------|---------|------|----------|
| **AHO** | `id_position = 45` | Tampilkan field HOPES (ppk, cix_no, main_cd, sub_cd, prom_dt) | Field HOPES wajib diisi |
| **Non-AHO** | `id_position ≠ 45` | Sembunyikan field HOPES | Field HOPES tidak divalidasi |

---

## 2. Kondisi Saat Ini (As-Is)

### 2.1 Backend — `EmployeeAgendaController::storeSubAgenda()` (line 267–313)

```php
// Saat ini validasi SELALU mewajibkan field HOPES untuk semua posisi:
$request->validate([
    'title'   => 'required',
    'ppk'     => 'required|string',   // ← wajib untuk SEMUA posisi
    'cix_no'  => 'required|string',   // ← wajib untuk SEMUA posisi
    'main_cd' => 'required|string',   // ← wajib untuk SEMUA posisi
    'sub_cd'  => 'required|string',   // ← wajib untuk SEMUA posisi
    ...
]);
```

**Masalah:** Karyawan non-AHO tidak bisa membuat sub-agenda sama sekali karena validasi HOPES selalu `required`.

### 2.2 Backend — `checkOut()` (line 348–505)

Sudah ada pola conditional yang benar berdasarkan keberadaan field:
```php
// Ini sudah benar — condisional berdasarkan data di DB
$imgRule = !empty($DEmpSubAgenda->ppk) ? 'required' : 'nullable';
if (!empty($DEmpSubAgenda->ppk)) { /* validasi HOPES */ }
```
→ Pola ini bisa dijadikan referensi untuk `storeSubAgenda`.

### 2.3 Backend — `SyncToHopesHdlr` (line 36–69)

Sudah ada guard clause yang benar:
```php
// Listener sudah aman — skip jika tidak ada ppk/cix_no
if (empty($subAgenda->ppk) || empty($subAgenda->cix_no)) {
    Log::channel('daily')->debug('HOPES sync skipped...');
    return;
}
```
→ Listener **tidak perlu diubah** untuk mendukung non-AHO.

### 2.4 Frontend — `ModalFormAgenda.vue`

Saat ini form **selalu menampilkan** section HOPES untuk semua karyawan (line 86–196).  
Tidak ada prop `id_position` yang dikirim ke komponen ini.

### 2.5 Tidak Ada Service Layer

Direktori `app/Services/` **belum ada** di codebase. Semua logika bisnis berada langsung di controller.

---

## 3. Analisis Opsi Arsitektur

### Opsi A: Dua Metode di Controller (Paling Minimal)

```php
// Di EmployeeAgendaController
public function storeSubAgenda(Request $request, DEmployeeAgenda $DEmployeeAgenda)
{
    $idPosition = $DEmployeeAgenda->employee->position_now->position->id;
    
    if ($idPosition === 45) {
        return $this->storeAHOSubAgenda($request, $DEmployeeAgenda);
    }
    return $this->storeDefaultSubAgenda($request, $DEmployeeAgenda);
}

private function storeAHOSubAgenda(...) { /* validasi + field HOPES */ }
private function storeDefaultSubAgenda(...) { /* validasi tanpa HOPES */ }
```

**Pro:** Perubahan minimal, tidak perlu file baru.  
**Kontra:** Controller makin gemuk (sudah 900+ baris), logika bisnis masih di controller, sulit di-test unit.

---

### Opsi B: Service Layer dengan Interface (Direkomendasikan)

Buat `app/Services/SubAgenda/` dengan dua concrete class dan satu interface:

```
app/Services/SubAgenda/
├── SubAgendaServiceInterface.php   ← Kontrak
├── SubAgendaService.php            ← Implementasi non-AHO
└── AHOSubAgendaService.php         ← Implementasi AHO (dengan HOPES)
```

Controller menjadi tipis — hanya menentukan service mana yang dipakai berdasarkan posisi:

```php
public function storeSubAgenda(Request $request, DEmployeeAgenda $DEmployeeAgenda)
{
    $this->authorize('create', [DEmpSubAgenda::class, $DEmployeeAgenda]);
    
    $idPosition = $DEmployeeAgenda->employee->position_now->position->id;
    $service = $idPosition === 45
        ? new AHOSubAgendaService()
        : new SubAgendaService();
    
    return $service->store($request, $DEmployeeAgenda);
}
```

**Pro:** Separation of concerns, mudah di-test, mudah dikembangkan (tambah posisi baru = tambah service baru).  
**Kontra:** Menambah file baru.

---

### Opsi C: Form Request Terpisah (Lightweight)

Buat dua Form Request class, controller menentukan mana yang dipakai:

```
app/Http/Requests/
├── StoreSubAgendaRequest.php        ← rules tanpa HOPES
└── StoreAHOSubAgendaRequest.php     ← rules dengan HOPES
```

Controller route tetap satu, validasi dibedakan sebelum proses simpan. Bisa dikombinasikan dengan Opsi A atau B.

---

## 4. Rekomendasi: Opsi B + Form Request Terpisah

Kombinasi paling seimbang antara keterbacaan, testability, dan scope perubahan.

### 4.1 Struktur File yang Dibuat

```
app/
├── Http/
│   └── Requests/
│       ├── StoreSubAgendaRequest.php        (baru)
│       └── StoreAHOSubAgendaRequest.php     (baru)
└── Services/
    └── SubAgenda/
        ├── SubAgendaServiceInterface.php    (baru)
        ├── SubAgendaService.php             (baru)
        └── AHOSubAgendaService.php          (baru)
```

File yang **diubah**:
- `app/Http/Controllers/EmployeeAgendaController.php` — `storeSubAgenda()` dan `updateSubAgenda()`
- `resources/js/components/ModalFormAgenda.vue` — tambah prop `id_position`, kondisikan HOPES section

File yang **tidak perlu diubah**:
- `app/Listeners/SyncToHopesHdlr.php` ✓ (sudah ada guard clause)
- `app/Jobs/PostToHopesJob.php` ✓
- `app/Events/OnSubAgendaCompleted.php` ✓
- `app/Models/DEmpSubAgenda.php` ✓
- Semua migration ✓

---

### 4.2 Rancangan Interface

```php
// app/Services/SubAgenda/SubAgendaServiceInterface.php
namespace App\Services\SubAgenda;

use App\Models\DEmployeeAgenda;
use Illuminate\Http\Request;

interface SubAgendaServiceInterface
{
    public function store(Request $request, DEmployeeAgenda $agenda);
    public function update(Request $request, \App\Models\DEmpSubAgenda $subAgenda);
}
```

---

### 4.3 Rancangan SubAgendaService (Non-AHO)

```php
// app/Services/SubAgenda/SubAgendaService.php
class SubAgendaService implements SubAgendaServiceInterface
{
    public function store(Request $request, DEmployeeAgenda $agenda)
    {
        $request->validate([
            'title' => 'required',
            // TIDAK ADA field HOPES
        ]);

        return DEmpSubAgenda::create([
            'id_emp_agenda' => $agenda->id,
            'title'         => $request->input('title'),
            // ppk, cix_no, main_cd, sub_cd → null (kolom nullable)
        ]);
    }

    public function update(Request $request, DEmpSubAgenda $subAgenda)
    {
        $request->validate([
            'title' => 'required',
        ]);

        $subAgenda->update([
            'title' => $request->input('title'),
        ]);

        return $subAgenda;
    }
}
```

---

### 4.4 Rancangan AHOSubAgendaService (AHO + HOPES)

```php
// app/Services/SubAgenda/AHOSubAgendaService.php
class AHOSubAgendaService implements SubAgendaServiceInterface
{
    public function store(Request $request, DEmployeeAgenda $agenda)
    {
        $request->validate([
            'title'   => 'required',
            'ppk'     => 'required|string',
            'cix_no'  => 'required|string',
            'cust_nm' => 'nullable|string',
            'main_cd' => 'required|string',
            'sub_cd'  => 'required|string',
            'prom_dt' => 'nullable|date|required_if:sub_cd,100',
        ], [
            'main_cd.required' => 'The Status Kunjungan field is required.',
            'sub_cd.required'  => 'The Sub Status field is required.',
        ]);

        return DEmpSubAgenda::create([
            'id_emp_agenda' => $agenda->id,
            'title'         => $request->input('title'),
            'ppk'           => $request->input('ppk'),
            'cix_no'        => $request->input('cix_no'),
            'cust_nm'       => $request->input('cust_nm'),
            'main_cd'       => $request->input('main_cd', '10'),
            'sub_cd'        => $request->input('sub_cd'),
            'prom_dt'       => $request->input('prom_dt'),
        ]);
    }

    public function update(Request $request, DEmpSubAgenda $subAgenda)
    {
        $request->validate([
            'title'   => 'required',
            'ppk'     => 'required|string',
            'cix_no'  => 'required|string',
            'cust_nm' => 'nullable|string',
            'main_cd' => 'required|string',
            'sub_cd'  => 'required|string',
            'prom_dt' => 'nullable|date|required_if:sub_cd,100',
        ]);

        $subAgenda->update([
            'title'   => $request->input('title'),
            'ppk'     => $request->input('ppk'),
            'cix_no'  => $request->input('cix_no'),
            'cust_nm' => $request->input('cust_nm'),
            'main_cd' => $request->input('main_cd'),
            'sub_cd'  => $request->input('sub_cd'),
            'prom_dt' => $request->input('prom_dt'),
        ]);

        return $subAgenda;
    }
}
```

---

### 4.5 Perubahan Controller

```php
// EmployeeAgendaController::storeSubAgenda() — setelah refactor
public function storeSubAgenda(Request $request, DEmployeeAgenda $DEmployeeAgenda)
{
    $this->authorize('create', [DEmpSubAgenda::class, $DEmployeeAgenda]);

    // Tentukan service berdasarkan posisi employee
    $idPosition  = $DEmployeeAgenda->employee->position_now->position->id;
    $service     = $idPosition === 45
        ? new AHOSubAgendaService()
        : new SubAgendaService();

    $subAgenda = $service->store($request, $DEmployeeAgenda);

    // Activity log — tidak berubah
    $employee   = Employee::find($DEmployeeAgenda->id_employee);
    $logMessage = "new task from agenda ({$DEmployeeAgenda->agenda_date}) - {$subAgenda->title} created by {$employee->fullname}";
    event(new OnLoggingActivity('emp_agenda', $logMessage, $employee));

    if ($request->accepts('application/json')) {
        return response()->json(['status' => 200, 'message' => "Successfull add new task.", 'subAgenda' => $subAgenda]);
    }
    return Redirect::route('agenda.index')->with('success', "Successfull add new task.");
}
```

---

### 4.6 Perubahan Frontend — ModalFormAgenda.vue

Komponen perlu menerima prop `id_position` dari parent page agar bisa conditionally render HOPES section.

```vue
<!-- ModalFormAgenda.vue — tambah prop -->
props: {
  indexSubAgenda: { type: Object, default() { return {}; } },
  errors:         { type: Object, default: {} },
  idAgenda:       Number,
  idPosition:     { type: Number, default: null }   // ← BARU
},
computed: {
  isAHO() {
    return this.idPosition === 45;
  },
  // ...existing computed
},
```

```vue
<!-- Wrap HOPES section dengan v-if -->
<template v-if="isAHO">
  <hr class="my-3" />
  <p class="font-weight-bold text-muted mb-3">HOPES - Hana on AHO</p>
  <!-- field PPK, CIX_NO, main_cd, sub_cd, prom_dt -->
</template>
```

Di parent page (misal `subAgenda.vue`), kirim `id_position` ke komponen:
```vue
<ModalFormAgenda
  :id-agenda="agendaId"
  :id-position="currentEmployee.id_position"
  ...
/>
```

---

## 5. Dampak Terhadap Komponen Lain

| Komponen | Perlu Diubah? | Alasan |
|----------|--------------|--------|
| `SyncToHopesHdlr` | **Tidak** | Guard clause `empty($subAgenda->ppk)` sudah menangani non-AHO |
| `PostToHopesJob` | **Tidak** | Hanya dijalankan jika ada ppk |
| `checkOut()` | **Tidak** | Sudah conditional berdasarkan `!empty($DEmpSubAgenda->ppk)` |
| `hopesResync()` | **Tidak** | Hanya dipanggil dari UI AHO |
| `hopesSelectCust()` | **Tidak** | Hanya dipanggil dari form AHO |
| `DEmpSubAgenda` model | **Tidak** | Kolom HOPES tetap nullable |
| Migration | **Tidak** | Kolom HOPES sudah nullable |

---

## 6. Pertimbangan Tambahan

### 6.1 Konstanta untuk id_position AHO

Hindari magic number `45` tersebar di codebase. Pertimbangkan konstanta atau config:

```php
// Opsi 1: konstanta di model
// app/Models/Ref_Position.php
const ID_AHO = 45;

// Opsi 2: config
// config/positions.php
return ['aho' => 45, 'bm' => 44, 'am' => 94, 'hc' => 96];
```

Sudah ada preseden untuk ini — `[44, 94, 96]` di `checkOut()` (line 400) saat ini menggunakan array literal.

### 6.2 updateSubAgenda()

Method `updateSubAgenda()` (line 336+) juga perlu mendapat perlakuan yang sama:  
saat ini ada validasi HOPES di dalamnya yang perlu dikondisikan berdasarkan posisi.  
Gunakan service yang sama (`$service->update(...)`) setelah service selesai dibuat.

### 6.3 Mobile API

Routes di `api.php` menggunakan controller yang sama. Setelah controller diubah,  
mobile API otomatis mengikuti — tidak perlu perubahan terpisah.  
Namun perlu dipastikan request mobile juga mengirim konteks posisi dengan benar.

### 6.4 Kolom HOPES Tetap Nullable

Kolom `ppk`, `cix_no`, `main_cd`, `sub_cd`, `prom_dt` di tabel `d_emp_sub_agendas`  
harus tetap `nullable` (sudah demikian). Sub-agenda non-AHO akan memiliki null di kolom ini.

---

## 7. Urutan Implementasi yang Disarankan

1. **Buat service files** (`SubAgendaServiceInterface`, `SubAgendaService`, `AHOSubAgendaService`)
2. **Refactor `storeSubAgenda()`** di controller untuk gunakan service
3. **Refactor `updateSubAgenda()`** di controller untuk gunakan service
4. **Update `ModalFormAgenda.vue`** — tambah prop `id_position`, kondisikan HOPES section
5. **Update parent pages** yang menggunakan `ModalFormAgenda` — kirim `id_position`
6. **Test manual** dengan akun AHO (id_position=45) dan non-AHO
7. **Review `checkOut()`** — sudah conditional, pastikan masih benar setelah refactor

---

## 8. Ringkasan

```
SEBELUM:
  storeSubAgenda() ──→ validasi HOPES (required untuk semua) ──→ simpan
  ModalFormAgenda  ──→ tampilkan HOPES section untuk semua

SESUDAH:
  storeSubAgenda() ──→ cek id_position ──→ AHOSubAgendaService (validasi HOPES)
                                       └─→ SubAgendaService (tanpa HOPES)
  ModalFormAgenda  ──→ isAHO = (id_position === 45)
                   ──→ v-if="isAHO" pada HOPES section
```

Perubahan **terlokalisasi** — tidak menyentuh event system, job, listener, atau migration.
