# Modul Penjualan (Laporan Shift)

## Flow

Form menampilkan semua nozzle dalam 1 shift, dikelompokkan per Produk.

**Status:**
```
Draft → Submitted → Approved → Locked
            ↑           ↓
         Recall      Rejected → edit → Re-submitted
                                           ↑
                              Unlock (by approver) — requires alasan
```

**Approved = Locked:** tidak bisa diedit, tidak bisa tambah/hapus foto, tidak bisa didelete.
Hanya user dengan `penjualan:approve` bisa Unlock. Tercatat di `unlocked_by_id`, `unlocked_at`, `unlock_reason`.

**Recall:** Operator SUBMITTED → DRAFT selama admin belum act. Tercatat di `recalled_by_id/recalled_at`.

## Aturan Teller

- `primary_teller` per nozzle → acuan volume (default: manual)
- `flag_reset = true` → teller awal boleh < teller akhir shift sebelumnya
- Teller awal = `teller_terakhir` dari shift sebelumnya (otomatis). Shift pertama = input manual.
- Volume dihitung dari `primary_teller` saat save.
- Anomali `METER_DISCREPANCY`: selisih manual vs digital > `teller_discrepancy_threshold_pct` (min 1 L, default 0.3%)

## Kas & Pembayaran

- Section pecahan tunai: 100k→1k (lembar) + logam (total Rp) + non-tunai: kartu, QR, instansi kredit
- Validasi frontend (warning, bukan hard-block): total kas + non-kas = total nilai penjualan shift
- Kolom di DB: `kas_100k..kas_logam`, `pembayaran_kartu`, `pembayaran_qr`, `pembayaran_instansi` (Numeric(15,3), default 0)
- Save laporan_shift → auto-upsert penyetoran via `_upsert_penyetoran()` (lihat modul-penyetoran.md)

> ⚠️ **`kas_100k`..`kas_1k` menyimpan JUMLAH LEMBAR (bill count), bukan nilai Rupiah.**
> Untuk mendapatkan nilai Rupiah, kalikan dengan denominasi:
> `grand_total = kas_100k×100000 + kas_50k×50000 + kas_20k×20000 + kas_10k×10000 + kas_5k×5000 + kas_2k×2000 + kas_1k×1000 + kas_logam`
> `kas_logam`, `pembayaran_kartu`, `pembayaran_qr`, `pembayaran_instansi` → sudah Rupiah langsung.

## Foto Referensi

- Upload via `POST /extract-from-image` (multipart/form-data: `file`, `old_foto_url?`)
- Backend upload ke storage DULU, baru AI extraction — `foto_url` selalu dikembalikan meski AI gagal
- Jika `old_foto_url` dikirim, file lama dihapus dari storage (replace, bukan append)
- URL disimpan di `laporan_shift.source_foto_url` saat save laporan
- Frontend: gunakan `getFileUrl(source_foto_url)` untuk display → routing ke `/api/v1/files/...` (authenticated)
- Delete draft laporan → file otomatis dihapus dari storage

## Activity Log

- Full history tersedia via `GET /api/v1/spbus/{spbu_id}/laporan-shift/{id}/activity-log`
- Membaca dari tabel `audit_log` (bukan dari field di `laporan_shift`)
- Events yang dilog: `submit`, `recall`, `approve`, `reject`, `unlock`
- Frontend: `useLaporanActivityLog(spbuId, laporanId)` → pass sebagai `auditEntries` ke `<AuditTimeline>`

## Submit Flow

- **"Save & Submit" button**: validasi dulu, tampilkan modal konfirmasi → TIDAK save sebelum modal
- User klik "Cancel" di modal → tutup modal, form tetap terbuka, **tidak ada yang tersimpan**
- User klik "Ya, Submit" → `doSave()` + `submit()` → navigate ke detail view

## `force_draft` (dev-only)

Query param `?force_draft=true` pada POST laporan-shift → skip auto-approve meskipun user punya permission.
Frontend kirim ini via `useCreateLaporan(spbuId, !canApprove)` untuk dev role switcher.

## Schema

```sql
laporan_shift (id, spbu_id, shift_id, tanggal, status,
               submitted_by_id, submitted_at, reviewed_by_id, reviewed_at,
               recalled_by_id, recalled_at, catatan_review,
               unlocked_by_id, unlocked_at, unlock_reason,
               kas_100k, kas_50k, kas_20k, kas_10k, kas_5k, kas_2k, kas_1k, kas_logam,
               pembayaran_kartu, pembayaran_qr, pembayaran_instansi)
-- UniqueConstraint: (spbu_id, shift_id, tanggal)

penjualan_nozzle (id, laporan_shift_id, nozzle_id,
                  teller_awal_manual, teller_akhir_manual,
                  teller_awal_digital, teller_akhir_digital,
                  flag_reset_teller, volume, harga_jual, nilai)

penjualan_transaksi (...)  -- ❌ BELUM DIIMPLEMENTASIKAN (CSV POS)
```

## Schema

```sql
laporan_shift (id, spbu_id, shift_id, tanggal, status,
               submitted_by_id, submitted_at, reviewed_by_id, reviewed_at,
               recalled_by_id, recalled_at, catatan_review,
               unlocked_by_id, unlocked_at, unlock_reason,
               source_foto_url,
               kas_100k, kas_50k, kas_20k, kas_10k, kas_5k, kas_2k, kas_1k, kas_logam,
               pembayaran_kartu, pembayaran_qr, pembayaran_instansi)
-- UniqueConstraint: (spbu_id, shift_id, tanggal)

penjualan_nozzle (id, laporan_shift_id, nozzle_id,
                  teller_awal_manual, teller_akhir_manual,
                  teller_awal_digital, teller_akhir_digital,
                  flag_reset_teller, volume, harga_jual, nilai)

penjualan_transaksi (...)  -- ❌ BELUM DIIMPLEMENTASIKAN (CSV POS)
```

## Endpoints

```
GET/POST  /api/v1/spbus/{id}/laporan-shift
GET       /api/v1/spbus/{id}/laporan-shift/teller-init
POST      /api/v1/spbus/{id}/laporan-shift/extract-from-image   ← multipart: file + old_foto_url?
PATCH     /api/v1/spbus/{id}/laporan-shift/{id}/submit
PATCH     /api/v1/spbus/{id}/laporan-shift/{id}/recall
PATCH     /api/v1/spbus/{id}/laporan-shift/{id}/review
PATCH     /api/v1/spbus/{id}/laporan-shift/{id}/unlock
GET       /api/v1/spbus/{id}/laporan-shift/{id}/activity-log    ← full audit trail dari audit_log
GET       /api/v1/spbus/{id}/laporan-shift/{id}/ba-pdf
```

## Files

- Router: `backend/app/routers/laporan_shift.py`
- Service: `backend/app/services/operational_service.py`
- Schema: `backend/app/schemas/operational.py`
- Frontend form: `frontend/src/components/forms/penjualan-form.tsx` — exports: `NozzleTable`, `KasPaymentSection`, `PenjualanFormContent`
- Frontend page: `frontend/src/app/(dashboard)/penjualan/penjualan-client.tsx`
- API hooks: `frontend/src/lib/hooks/usePenjualan.ts` — termasuk `useLaporanActivityLog`
