# CHM-D-20260612-71 — Pekerjaan Client CHM (12 Juni 2026)

## Pekerjaan 1 — Konfigurasi Default Jatuh Tempo Penjualan

### Context
Saat ini field "Jatuh Tempo" pada form penjualan diisi manual oleh pengguna tanpa nilai awal. Permintaan: tambahkan pengaturan default jatuh tempo (dalam hari) yang bisa dikonfigurasi di menu Pengaturan → Transaksi. Ketika pengguna mengisi tanggal penjualan, tanggal jatuh tempo otomatis terisi dengan tanggal penjualan + N hari sesuai konfigurasi (bisa diubah manual setelahnya).

### Pendekatan Penyimpanan
Tambahkan kolom `default_jatuh_tempo_hari` ke tabel `company_profiles` yang sudah ada. Model `CompanyProfile` sudah memiliki pola singleton `current()` dan digunakan di seluruh aplikasi — tidak perlu tabel/model baru.

### Komponen yang Diubah

#### 1. Migration
`database/migrations/2026_06_12_000001_add_default_jatuh_tempo_to_company_profiles.php`
```php
$table->unsignedSmallInteger('default_jatuh_tempo_hari')->nullable()->after('website');
```

#### 2. Model — `app/Models/CompanyProfile.php`
Tambah `'default_jatuh_tempo_hari'` ke `$fillable`.

#### 3. Route — `routes/web.php`
Di dalam group `pengaturan.`:
```php
Route::get('transaksi',  [TransaksiController::class, 'index']) ->name('transaksi');
Route::post('transaksi', [TransaksiController::class, 'update'])->name('transaksi.update');
```

#### 4. Controller — `app/Http/Controllers/Pengaturan/TransaksiController.php`
- `index()`: load `CompanyProfile::current()`, return view `pengaturan.transaksi`
- `update()`: validasi `default_jatuh_tempo_hari` nullable|integer|min:1|max:365, simpan, `clearCache()`, redirect back with success

#### 5. View — `resources/views/pengaturan/transaksi.blade.php`
Halaman pengaturan sederhana, satu section "Penjualan" dengan field:
- **Default Jatuh Tempo (hari)**: input number, placeholder "Contoh: 30", min 1 max 365
- Tombol Simpan

Ikuti pola tampilan `profil-perusahaan.blade.php` (card, form POST, flash success).

#### 6. Menu — `config/menu.php`
Tambah submenu "Transaksi" di dalam children "Pengaturan":
```php
[
    'title' => 'Transaksi',
    'route' => 'pengaturan.transaksi',
],
```
Posisikan sebelum "Profil Perusahaan".

#### 7. Penjualan Form — auto-fill jatuh tempo
**`app/Http/Controllers/Penjualan/PenjualanController.php`**

Method `create()`: tambah `$defaultJatuhTempo` ke compact:
```php
$defaultJatuhTempo = CompanyProfile::current()->default_jatuh_tempo_hari;
return view('penjualan.form', compact('nomor', 'faskesList', 'defaultJatuhTempo'));
```

Method `edit()`: sama, pass `$defaultJatuhTempo` (untuk pre-fill jika jatuh tempo belum diset).

**`resources/views/penjualan/form.blade.php`**

Tambah JS constant:
```js
const DEFAULT_JATUH_TEMPO = {{ $defaultJatuhTempo ?? 'null' }};
```

Event listener pada `tanggalPenjualan` (flatpickr `onChange`): jika `DEFAULT_JATUH_TEMPO` tersedia → auto-fill selalu (tidak ada guard, agar bisa update ulang saat tanggal penjualan diganti):
```js
// di onChange flatpickr tanggalPenjualan:
if (!DEFAULT_JATUH_TEMPO || !selectedDates.length) return;
const d = new Date(selectedDates[0]);
d.setDate(d.getDate() + DEFAULT_JATUH_TEMPO);
fpJatuhTempo.setDate(d);
```
Pada mode edit (`isset($penjualan)`): jatuh tempo sudah terisi dari data, tapi bisa di-update jika user ganti tanggal penjualan.

### Verifikasi
1. Buka Pengaturan → Transaksi → isi "30" → Simpan → flash success muncul
2. Buka form Buat Penjualan → pilih tanggal penjualan → field Jatuh Tempo otomatis terisi +30 hari
3. Ganti tanggal penjualan → jatuh tempo ikut berubah
4. Kosongkan pengaturan (hapus nilai) → field jatuh tempo tidak auto-fill

---

## Pekerjaan 2 — Tampilkan Nomor PO & Restrukturisasi Tanggal di Header Penjualan

### Context
Header form penjualan saat ini hanya memiliki satu tanggal (`tanggal_penjualan`) yang berfungsi ganda sebagai tanggal transaksi sekaligus tanggal penjualan. Permintaan:
- **Tanggal Transaksi** — diambil dari `tanggal_penjualan` lama, otomatis diisi saat transaksi dibuat, **read-only** (tidak bisa diubah)
- **Tanggal Penjualan** — field baru, kosong awal, dipilih user; ketika dipilih → jatuh tempo auto-fill berdasarkan pengaturan
- **Tanggal Kirim** — field baru, opsional
- **Nomor PO** — field baru, opsional, referensi PO dari customer

### Perubahan Database

**Migration** `2026_06_12_000002_restructure_penjualans_tanggal.php`:
1. Tambah `tanggal_transaksi` DATE NOT NULL → isi dari nilai existing `tanggal_penjualan`
2. Ubah `tanggal_penjualan` → nullable
3. Tambah `nomor_po` VARCHAR(50) nullable
4. Tambah `tanggal_kirim` DATE nullable

### Perubahan Kode

**`app/Models/Penjualan.php`**
- Tambah ke `$fillable`: `tanggal_transaksi`, `nomor_po`, `tanggal_kirim`
- Tambah ke `$casts`: `tanggal_transaksi => 'date'`, `tanggal_kirim => 'date'`

**`app/Http/Controllers/Penjualan/PenjualanController.php`**
- `store()`: `tanggal_transaksi` → set otomatis ke `today()`, tambah `nomor_po`, `tanggal_kirim`
- `update()`: sama, tapi `tanggal_transaksi` **tidak ikut diupdate** (read-only permanent)
- `data()`: filter & `tanggal_fmt` pakai `tanggal_transaksi`

**`resources/views/penjualan/form.blade.php`**

Layout baris 1 (col-2 | col-2 | col-5 | col-3):
| Field | Keterangan |
|---|---|
| Nomor Penjualan | read-only |
| Tgl. Transaksi | read-only, auto-filled today |
| Fasilitas Kesehatan | select2 |
| No. PO | text, opsional |

Layout baris 2 (col-2 | col-2 | col-2 | col-6):
| Field | Keterangan |
|---|---|
| Tgl. Penjualan | flatpickr |
| Tgl. Kirim | flatpickr, opsional |
| Jatuh Tempo | flatpickr, auto-fill |
| Catatan | textarea |

Field **Tenaga Kesehatan dihapus** dari form.

---

## Pekerjaan 3 — PDF Improvements

### Perubahan `pdf.blade.php` & `pdf-gabungan.blade.php`
- Hapus tanggal di bawah judul "FAKTUR PENJUALAN" di kop
- Hapus baris Tenaga Kesehatan
- Hapus baris Tgl. Transaksi dan Tgl. Kirim
- No. PO menggantikan posisi No. Faktur
- `kode_ref` menggantikan kode barang di kolom tabel
- Bulan bahasa Indonesia: `->locale('id')->translatedFormat('d F Y')`

---

## Pekerjaan 4 — Cetakan Surat Jalan

### Database
- Migration `2026_06_12_000003`: tambah `nomor_surat_kirim` → kemudian di-rename
- Migration `2026_06_12_000004`: rename `nomor_surat_kirim` → `nomor_surat_jalan`

### Nomor Surat Jalan
Format: `SK/yyyy/mm/0001` — di-generate otomatis saat `selesaikan()`.

### `app/Models/Penjualan.php`
Tambah `generateNomorSuratJalan()`: prefix `SK/`, pola sama dengan `generateNomor()`.

### `app/Http/Controllers/Penjualan/PenjualanController.php`
- `selesaikan()`: generate `nomor_surat_jalan` via `Penjualan::generateNomorSuratJalan()`
- `suratJalan($id)`: abort jika draft, auto-generate nomor jika null, render view `penjualan.surat-jalan` PDF

### `resources/views/penjualan/surat-jalan.blade.php` (baru)
- Header: kop perusahaan, judul "SURAT JALAN"
- Info: Kepada/Faskes, Alamat, No. PO (conditional), Tgl. Kirim (conditional)
- Tabel: No, Nama Barang + kode_ref sub-info, Merek, Ukuran, Qty, Satuan
- TTD: Pengirim (tanpa nama), Diterima Oleh (tanpa nama + "Nama & Stempel"), Mengetahui (nama user verifikasi fisik)

---

## Pekerjaan 5 — PDF Preview Modal (bukan new tab)

### `resources/views/penjualan/index.blade.php`
- Modal `#modalPdfPreview` dengan iframe `#iframePdf`
- Fungsi `bukaPdfModal(url)` — load URL ke iframe, buka modal

### `resources/views/penjualan/show.blade.php`
- Modal surat jalan `#modalSuratJalan` dengan iframe `#iframeSuratJalan`
- Fungsi `bukaSuratJalanModal()`

---

## Pekerjaan 6 — Sistem Permission Granular Penjualan

### 3 Permission Baru
| Permission | Akses |
|---|---|
| `penjualan.transaksi` | Buat draft, edit, selesaikan, hapus |
| `penjualan.pajak` | Upload/hapus faktur PPN, lanjut pengiriman |
| `penjualan.gudang` | Verifikasi fisik, cetak PDF, surat jalan |

### Database
- Migration `2026_06_12_000005`: insert 3 permission ke tabel `permissions` (guard `internal`)
- Migration `2026_06_12_000006`: pindah assignment dari `model_has_permissions` (salah) ke `role_has_permissions` untuk admin role — Spatie menyimpan role-permission di `role_has_permissions`, bukan `model_has_permissions`

### `database/seeders/RolePermissionSeeder.php`
- Ganti permission lama (`penjualan.view`, `penjualan.create`, dll.) dengan 3 permission baru
- Admin: dapat semua via `syncPermissions(all)`
- staf_gudang/kepala_gudang: `penjualan.gudang`
- staf_sales/kepala_sales: `penjualan.transaksi`

### Routes — `routes/web.php`
- Group middleware: `permission:penjualan.transaksi|penjualan.pajak|penjualan.gudang|internal`
- Per-route middleware sesuai permission masing-masing
- Route `/{id}` (show/wildcard) dipindah ke **paling bawah** agar tidak menangkap `/create`, `/pdf-gabungan`, dll.

### Menu — `config/menu.php`
Menu Penjualan hanya satu submenu: **Transaksi Penjualan** (permission `penjualan.transaksi`). Submenu Pajak dan Gudang dihapus.

---

## Pekerjaan 7 — Alur Status Penjualan Baru

### Alur Status
```
draft → [Selesaikan] → selesai → [Lanjut Pengiriman] → verifikasi_fisik → [Konfirmasi Verifikasi] → terverifikasi
```

### Label Display
| Status DB | Label Tampil | Warna Badge |
|---|---|---|
| `draft` | Draft | abu-abu |
| `selesai` | Upload eFaktur PPN | kuning (warning) |
| `verifikasi_fisik` | Verifikasi Fisik | biru (info) |
| `terverifikasi` | Selesai | hijau (success) |

### Database
- Migration `2026_06_15_000001`: update PostgreSQL check constraint `penjualans_status_check` menambah nilai `verifikasi_fisik`

### Model — `app/Models/Penjualan.php`
Tambah method:
```php
public function isVerifikasiFisik(): bool { return $this->status === 'verifikasi_fisik'; }
```

### Controller — `app/Http/Controllers/Penjualan/PenjualanController.php`
- Tambah method `lanjutPengiriman($id)`: cek `isSelesai()`, update status ke `verifikasi_fisik`
- `verifikasi()`: guard diubah dari `isSelesai()` → `isVerifikasiFisik()`
- `scanVerifikasi()`: guard diubah dari `isSelesai()` → `isVerifikasiFisik()`
- `konfirmasiVerifikasi()`: guard diubah dari `isSelesai()` → `isVerifikasiFisik()`, status akhir tetap `terverifikasi` (ditampilkan sebagai "Selesai")

### Route Baru
```php
Route::post('/{id}/lanjut-pengiriman', [PenjualanController::class, 'lanjutPengiriman'])
    ->name('lanjut-pengiriman')
    ->middleware('permission:penjualan.pajak|internal');
```

### Tombol di `show.blade.php`
| Kondisi | Tombol |
|---|---|
| `isDraft()` + `penjualan.transaksi` | Selesaikan, Edit, Hapus |
| `isSelesai()` + `penjualan.pajak` | Lanjut Pengiriman (kuning) |
| `isVerifikasiFisik()` + `penjualan.gudang` | Verifikasi Fisik (link ke /verifikasi) |

### Card Faktur PPN
Hanya tampil saat status `selesai`, `verifikasi_fisik`, atau `terverifikasi` (bukan draft).

### Tabel Index
- Kolom aksi: hanya tampil tombol Lihat Detail, Edit (jika draft), Surat Jalan (jika bukan draft), Gabungan PDF (jika ada faktur)
- Tombol Cetak Faktur dan Verifikasi Fisik dihapus dari tabel

---

## Bug Fixes

### Route Order (wildcard `/{id}` menangkap `/create`)
**Masalah**: Route `GET /{id}` (show) terdaftar sebelum `GET /create`, sehingga URL `/create` ditangkap sebagai ID.
**Fix**: Pindahkan route `/{id}` (show) ke posisi paling bawah dalam group, setelah semua route statis.

### Sweet-alerts.js Error Global
**Masalah**: `resources/assets/js/sweet-alerts.js` (file demo tema Ynex) dimuat global di `master.blade.php` dan mencari elemen DOM yang tidak ada di halaman biasa → `Uncaught TypeError: Cannot set properties of null (setting 'onclick')`.
**Fix**: Hapus `@vite('resources/assets/js/sweet-alerts.js')` dari `master.blade.php`.

### Spatie Permission — Role Assignment ke Tabel Salah
**Masalah**: Migration `000005` insert ke `model_has_permissions` dengan `model_type = 'App\Models\Role'`, padahal Spatie menyimpan role-permission di tabel `role_has_permissions`.
**Fix**: Migration `000006` memindahkan ke `role_has_permissions` dan membersihkan baris salah.

### PostgreSQL Check Constraint Status
**Masalah**: Kolom `status` punya constraint `CHECK (status IN ('draft','selesai','terverifikasi'))` — nilai `verifikasi_fisik` ditolak.
**Fix**: Migration `2026_06_15_000001` drop dan recreate constraint dengan tambahan `verifikasi_fisik`.

---

## Pekerjaan 8 — Minta Revisi Penjualan

### Context
Ketika penjualan sudah diselesaikan (status `selesai` atau `verifikasi_fisik`), stok sudah berkurang. Jika ada kesalahan data, user perlu bisa mengembalikan penjualan ke `draft` agar bisa dikoreksi, dengan audit trail siapa yang minta revisi, kapan, dan alasannya.

### Database
**Migration** `2026_06_15_000002_add_revisi_fields_to_penjualans.php`:
```php
$table->unsignedBigInteger('direvisi_oleh')->nullable();
$table->timestamp('direvisi_at')->nullable();
$table->text('alasan_revisi')->nullable();
$table->foreign('direvisi_oleh')->references('id')->on('users_internals')->onDelete('set null');
```

### Model — `app/Models/Penjualan.php`
- Tambah ke `$fillable`: `direvisi_oleh`, `direvisi_at`, `alasan_revisi`
- Tambah ke `$casts`: `'direvisi_at' => 'datetime'`
- Tambah relationship: `direvisiOleh()` → `belongsTo(UserInternal::class, 'direvisi_oleh')`

### Controller — `app/Http/Controllers/Penjualan/PenjualanController.php`
Method `mintaRevisi($id, Request $request)`:
1. Validate `alasan` required|string|max:1000
2. Abort jika status bukan `selesai` atau `verifikasi_fisik`
3. DB::transaction:
   - Query `KartuStok` where `referensi_type='penjualan'`, `referensi_id=$id`, `jenis='keluar'`
   - Untuk setiap entry: `$stok->increment('qty', qty_keluar)` + `KartuStok::catat(jenis='masuk')`
   - Update penjualan: `status='draft'`, `nomor_surat_jalan=null`, `direvisi_oleh`, `direvisi_at`, `alasan_revisi`

### Route — `routes/web.php`
```php
Route::post('/{id}/minta-revisi', [PenjualanController::class, 'mintaRevisi'])
    ->name('minta-revisi')
    ->middleware('permission:penjualan.transaksi|internal');
```

### View — `resources/views/penjualan/show.blade.php`
- **Tombol** "Minta Revisi" (btn-danger) muncul saat `isSelesai()`, `isVerifikasiFisik()`, atau `isVerified()` + permission `penjualan.transaksi`
- **Tombol Cetak Faktur & Cetak Surat Jalan** muncul saat `isVerifikasiFisik()` + permission `penjualan.gudang`
- **Modal** `#modalMintaRevisi`: textarea alasan, tombol Konfirmasi Revisi
- **Alert riwayat revisi**: muncul di atas tabel item jika `$penjualan->direvisi_at` terisi, menampilkan nama user (hitam), waktu, dan alasan
- **JS**: `doMintaRevisi()` buka modal, `submitMintaRevisi()` POST ke route dengan Swal loading + reload

### View — `resources/views/penjualan/index.blade.php`
- Tombol Cetak Faktur (`/pdf`) dan Surat Jalan muncul di kolom aksi saat `row.status === 'verifikasi_fisik'`
- Tombol merged PDF (`/pdf-merged`) hanya muncul jika `hasFaktur && row.status !== 'draft'`

### Selesaikan — Stok Otomatis dari Gudang Pusat
FEFO fallback di `selesaikan()` diubah agar hanya mengambil stok dari gudang dengan `jenis='pusat'`:
- Validasi ketersediaan stok: filter `gudang_id = gudangPusat->id`
- Decrement FEFO: filter yang sama, pesan error menyebutkan "di Gudang Pusat"
- Restore saat Minta Revisi: otomatis ke stok ID asal via `KartuStok.stok_id` — tidak perlu perubahan tambahan

### Bug Fix — Tombol File Muncul di Status Draft
**Masalah**: Penjualan yang pernah punya faktur PPN lalu di-revisi kembali ke draft masih menampilkan tombol merged PDF di tabel index.
**Fix**: Tambah kondisi `row.status !== 'draft'` pada render tombol merged PDF.
