@AGENTS.md

# SPBU Manager — Frontend (Next.js)

## File Structure

```
src/
├── proxy.ts                    ← Next.js 16 middleware (nama file & fungsi harus 'proxy')
├── app/
│   ├── (auth)/login/
│   ├── (dashboard)/
│   │   ├── layout.tsx          ← sidebar + header (Tabler) + MaintenancePoller
│   │   ├── dashboard/          ← ❌ TODO
│   │   ├── track/              ← Track Your Day  ✅ DONE
│   │   ├── penjualan/          ← ✅ DONE
│   │   ├── stock/              ← ✅ DONE
│   │   ├── penebusan/          ← ✅ DONE
│   │   ├── penerimaan/         ← ✅ DONE (+ approval flow + BAST PDF)
│   │   ├── expenses/           ← ✅ DONE (+ approval flow + BAST PDF)
│   │   ├── penyetoran/         ← ✅ DONE (per-shift + batch model + BAST PDF)
│   │   ├── rekonsiliasi/       ← ✅ DONE (2 tab: Harian placeholder + End-to-End)
│   │   ├── laporan/            ← ❌ TODO
│   │   ├── analytics/          ← ❌ TODO
│   │   ├── anomali/            ← ❌ TODO
│   │   ├── products/           ← ✅ DONE
│   │   ├── spbu/               ← ✅ DONE (list + create + settings)
│   │   └── users/              ← ✅ DONE (users + roles)
│   ├── maintenance/            ← ✅ DONE
│   └── not-found.tsx
├── components/
│   ├── ui/
│   │   ├── maintenance-poller.tsx
│   │   ├── sidebar.tsx
│   │   ├── topbar.tsx
│   │   ├── audit-timeline.tsx      ← AuditTimeline: chronological activity log (submit/recall/approve/unlock)
│   │   └── chat-widget.tsx         ← PUMP-kun AI chat widget
│   └── forms/
│       ├── penjualan-form.tsx      ← shared: NozzleTable, KasPaymentSection, PenjualanFormContent
│       ├── stock-form.tsx          ← shared: TankTable, StockFormContent
│       ├── spbu-threshold-form.tsx
│       ├── spbu-islands-form.tsx
│       └── spbu-tangki-form.tsx    ← CSV import kalibrasi
├── lib/
│   ├── api/client.ts           ← base axios instance
│   ├── hooks/                  ← useAuth, usePenjualan, useStock, dll
│   └── utils/
│       ├── format.ts           ← formatRupiah, formatNumber, formatDate, formatDateTime
│       ├── file-url.ts         ← getFileUrl(): /uploads/... → /api/v1/files/... (authenticated)
│       └── compress-image.ts
├── stores/
└── types/index.ts              ← semua TypeScript types
```

## URL Structure

```
/                         → redirect ke /dashboard
/login                    → no sidebar
/maintenance              → superadmin bypass via /login?staff=1

/dashboard                ← ❌ TODO
/track                    ← Track Your Day

/penjualan
/stock
/penebusan, /penebusan/baru, /penebusan/[id]
/penerimaan
/expenses
/penyetoran
/rekonsiliasi             ← tab Harian (❌ TODO) + tab End-to-End (✅)

/laporan                  ← ❌ TODO
/analytics                ← ❌ TODO
/anomali                  ← ❌ TODO

/products
/spbu, /spbu/baru
/spbu/[id]/settings       ← tabs: Informasi Dasar, Shift, Island & Nozzle,
                                   Tangki Pendam, Contracts, Threshold & Alert
/users, /users/roles
```

## Konvensi

- **UI**: Tabler CSS classes langsung — bukan Tailwind, bukan shadcn
- **Forms**: React Hook Form + Zod untuk semua form
- **Loading**: Tabler skeleton/spinner
- **Error**: Tabler alert component
- **Destructive actions**: modal konfirmasi — bukan `window.confirm`
- **Permission gate**: `<PermissionGuard modul="penjualan" aksi="create">` wrapper
- **Post-login navigation**: `window.location.href = '/'` (bukan `router.push`) agar proxy bisa intercept
- **Middleware**: file harus bernama `proxy.ts`, fungsi harus bernama `proxy()`
- **Audit trail display**: gunakan `<AuditTimeline>` dari `components/ui/audit-timeline.tsx` — bukan inline divs. Pass `auditEntries={activityLog}` (dari `useLaporanActivityLog`) untuk full history; tanpa prop itu, fallback ke field tunggal di record.
- **File URL**: selalu gunakan `getFileUrl(stored_url)` dari `lib/utils/file-url.ts` untuk display gambar/file yang diupload. Mengubah `/uploads/...` → `{API_BASE}/api/v1/files/...`. Jangan hard-code URL storage langsung di `<img src>`.
- **Kas bill counts**: `kas_100k`..`kas_1k` di `laporan_shift` menyimpan JUMLAH LEMBAR, bukan Rupiah. Kalikan dengan denominasi untuk mendapat nilai: `kas_100k × 100000`, dst. `kas_logam` dan `pembayaran_*` sudah Rupiah.
- **Approved = locked**: semua action button (edit, delete, upload foto) di-hide/disable ketika status bukan `draft`. Unlock button hanya tampil untuk user dengan permission `approve`.
- **`force_draft`**: `useCreateLaporan(spbuId, !canApprove)` dan `useCreateStock(spbuId, !canApprove)` — passing forceDraft param agar dev role switcher tidak auto-approve

## Shared Form Components (Track Your Day)

Track Your Day reuse komponen dari halaman individual — **tidak duplikasi kode**:

| Komponen | File | Props Kunci |
|----------|------|-------------|
| `PenjualanFormContent` | `components/forms/penjualan-form.tsx` | `hideShiftSelector`, `initialTanggal`, `initialShiftId` |
| `StockFormContent` | `components/forms/stock-form.tsx` | `hideShiftSelector`, `initialTanggal`, `initialShiftId` |
| `ExpenseForm` | `expenses-client.tsx` | di-export langsung |
| `NewDepositModal` | `penyetoran-client.tsx` | di-export langsung |

Track Your Day UI: per shift → collapsible card → 4 tab (Penjualan, Stock, Expenses, Penyetoran) → list view + "Add" button → modal popup reuse shared form.

## PUMP-kun AI Chat Widget

- File: `components/ui/chat-widget.tsx`
- Logo: `public/pump-kun.png` — PNG transparan, 78×78px di FAB, hover → bounce (`.pump-kun-bounce`)
- API: `POST /api/v1/assistant` → SSE atau JSON `{ response, tools_used[] }`

## Penyetoran — Per-Shift Batch Model

- Setiap `laporan_shift` punya satu `Penyetoran` (auto-created via `_upsert_penyetoran`)
- **Batch flow**: Operator pilih penyetoran (checkbox) → "Kirim ke Manager" → `PenyetoranBatch` DRAFT → submit batch → manager approve
- Status penyetoran item: `draft → submitted → approved`
- Status batch: `draft → submitted → approved`
- Hooks: `usePenyetoranList`, `usePenyetoranBatchList`, `useCreateBatch`, `useSubmitBatch`, `useReviewBatch`

## Business Rules & Status Flow

→ root `CLAUDE.md` §3 (roles/permission), §5 (flow per modul), §9 (semua rules)
