POS Grosir Dutra Store: update kasir/produk/riwayat/pengaturan + offline queue

This commit is contained in:
onie
2026-10-17 06:38:50 +07:00
parent bec02d196c
commit 0121eca8bb
15 changed files with 1202 additions and 524 deletions

View File

@@ -0,0 +1,183 @@
# Coding Standard — Dutra Store (POS Grosir Flutter)
Gunakan skill ini sebagai acuan setiap kali menulis, mengubah, atau meninjau kode aplikasi. Jika ada pertentangan, prioritaskan yang tercantum di sini.
## 1. Prinsip Umum
### 1.1 Clean Code
- Satu function/method lakukan satu hal saja (Single Responsibility).
- Nama variable/function harus deskriptif: `isPaying` bukan `flag`, `recordPayment` bukan `save`.
- Jangan duplikasi logic — extract ke function/helper yang reuseable.
- Komentar hanya untuk "mengapa", bukan "apa yang dilakukan". Kode harus self-explanatory untuk "apa".
### 1.2 File Organization
- **Satu file satu class utama**. Exception: helper kecil yang sangat spesifik untuk screen itu.
- Nama file: `snake_case.dart`. Nama class: `PascalCase`.
- Urutan import:
1. `dart:` core
2. `package:` third-party
3. `package:<project>/` internal
4. Relative import `./` atau `../`
- Jangan gunakan relative import untuk file di folder berbeda; gunakan package import.
### 1.3 Naming Convention
- **Class/Widget**: `PascalCase` → `KasirScreen`, `_CartBarState`
- **Private class/widget**: underscore prefix → `_CartBar`, `_ReceiptRow`
- **Function/method**: `camelCase` → `addToCart`, `_showReceipt`
- **Variable**: `camelCase` → `isPayingRef`, `payingStart`
- **Constant**: `lowercase with underscores` atau `PascalCase` sesuai konvensi project. Untuk warna di `colors.dart` pakai `lowercase_with_underscores`: `kAccent`, `kSuccess`.
- **Boolean**: pakai prefix `is`, `has`, `should`, `can` → `isLoading`, `hasError`, `shouldShow`
### 1.4 Null Safety
- Hindari `!` (force unwrap) kecuali benar-benar yakin tidak null.
- Gunakan `late` hanya untuk field yang di-init di `initState` atau setelah async yang dijamin selesai.
- Gunakan `?` (nullable) untuk parameter yang opsional.
- Gunakan `??` (default value) daripada ternary cek null.
- Jangan biarkan `dynamic` keculatau untuk JSON parsing yang benar-benar dinamis.
## 2. State Management (Provider)
### 2.1 AppState Structure
- `AppState` extends `ChangeNotifier`.
- Semua mutable state disimpan di `AppState`, bukan di widget state kecuali state lokal UI (misal `_search` TextEditingController).
- Gunakan `context.watch<AppState>()` untuk rebuild saat state berubah.
- Gunakan `context.read<AppState>()` untuk aksi tanpa rebuild (misal di `onPressed`).
- Jangan panggil `notifyListeners()` di dalam getter — getter harus pure.
### 2.2 Async Operations
- Semua async yang modifikasi state harus wrapped dengan try-catch.
- Gunakan `_withTimeout()` untuk semua operasi Supabase agar tidak hang selamanya.
- Set `notifyListeners()` di akhir operasi sukses maupun gagal agar UI tetap konsisten.
- Jangan lupa handle `mounted` check sebelum `setState` atau `notifyListeners` di dalam async callback.
### 2.3 Offline Queue
- Semua operasi tulis (insert/update/delete) ke Supabase harus menyimpan ke `_offlineQueue` jika gagal.
- Jangan duplicate queue logic — panggil `_offlineQueue.addToQueue()` di catch block.
- Saat online, `OfflineQueueService` akan otomatis retry.
## 3. Supabase & Database
### 3.1 Query Pattern
- Pakai `_withTimeout()` untuk semua query Supabase.
- Gunakan `.maybeSingle()` untuk query yang return 0 atau 1 row.
- Gunakan `.select()` dengan kolom spesifik kecuali butuh semua kolom.
- Jangan hardcode ID di query — gunakan parameter.
### 3.2 Error Handling
- Tangkap `PostgrestException` untuk error spesifik Supabase.
- Tangkap `TimeoutException` untuk timeout.
- Tangkap `Exception` umum sebagai fallback.
- Jangan expose stack trace ke user — tampilkan pesan yang ramah.
### 3.3 Transaction Safety
- Untuk operasi yang butuh atomic (misal checkout + update stock), lakukan di urutan yang benar:
1. Insert sale
2. Update stock untuk setiap item
3. Reload products + sales
- Jika salah satu gagal, rollback manual atau tambahkan ke offline queue.
## 4. Widget & UI Pattern
### 4.1 Widget Composition
- Prefer composition over inheritance.
- Ekstrak widget kecil yang berulang jadi `const` widget terpisah.
- Gunakan `const` constructor untuk semua widget yang immutable.
- Jangan buat `StatefulWidget` kecuali butuh state lokal.
### 4.2 StatefulWidget Best Practices
- State harus minimal — simpan hanya yang benar-benar perlu berubah.
- Jangan simpan objek kompleks di state tanpa dispose (misal controller, timer, animation).
- Semua `FocusNode`, `AnimationController`, `Timer`, `StreamSubscription` harus di-dispose.
- Gunakan `SingleTickerProviderStateMixin` untuk `AnimationController`, `TickerProviderStateMixin` untuk multiple.
### 4.3 Dialog & Bottom Sheet
- Gunakan `showDialog` + `AlertDialog` untuk dialog konfirmasi/form pendek.
- Gunakan `showModalBottomSheet` dengan `isScrollControlled: true` untuk form panjang atau list yang perlu scroll.
- Bungkus form dengan `SingleChildScrollView` + `padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom)`.
- Gunakan `StatefulBuilder` jika dialog butuh update UI internal (misal loading, toggle).
- Jangan pass `context` dari dialog ke parent untuk navigasi — pass context yang benar saat showDialog.
### 4.4 List & Grid
- Gunakan `ListView.builder` untuk list panjang, `ListView.separated` untuk list dengan separator.
- Gunakan `GridView.builder` untuk grid panjang.
- Tambahkan `shrinkWrap: true` dan `physics: NeverScrollableScrollPhysics()` jika list/grid di dalam `SingleChildScrollView`.
- Jangan lupa `itemExtent` atau `prototypeItem` jika list item heightnya fixed untuk performa.
### 4.5 Text & Format
- Semua angka mata uang **wajib** menggunakan `formatRupiah()`.
- Jangan hardcode string angka/uang di UI.
- Gunakan `TextOverflow.ellipsis` untuk teks yang bisa panjang.
- Gunakan `maxLines` untuk membatasi teks di `Row`/`Column` yang bisa overflow.
## 5. Error & Edge Case Handling
### 5.1 Mounted Check
- Setiap async callback yang akses `context` atau `setState` harus check `if (!mounted) return` sebelum akses.
- Jangan gunakan `mounted` untuk logic bisnis — hanya untuk guard UI update.
### 5.2 Empty/Null State
- Selalu handle kasus data kosong: `if (products.isEmpty)`, `if (cart.isEmpty)`.
- Tampilkan empty state yang informatif, bukan cuma `Container()` atau `SizedBox.shrink()`.
### 5.3 Boundary Value
- Qty minimum: 1 (jika <= 0, remove dari cart).
- Harga minimum: 0 (jika <= 0, tampilkan error atau disable button).
- Stok: clamp ke `max(0, stok - qty)` saat checkout.
- Diskon: clamp ke `min(diskon, subtotal)`.
## 6. Testing & Verification
### 6.1 Static Analysis
- Jalankan `flutter analyze` sebelum commit.
- Target: **No issues found** (tanpa error, warning yang bisa diperbaiki).
- Dead code harus dihapus, tidak di-`//` comment.
### 6.2 Manual Testing Checklist
- [ ] Test flow lengkap: login → kasir → tambah produk → bayar → struk → riwayat → detail.
- [ ] Test role kasir: akses yang disembunyikan benar-benar hilang.
- [ ] Test role admin: semua aksi admin muncul dan berfungsi.
- [ ] Test offline: matikan wifi, buat transaksi, hidupkan wifi, cek sync.
- [ ] Test keyboard: input uang/diskon dengan keyboard muncul.
- [ ] Test small screen: HP 5" atau emulator 360x640.
- [ ] Test landscape (jika applicable).
- [ ] Test rotation (jika applicable).
### 6.3 Performance
- Jangan rebuild seluruh list saat satu item berubah — gunakan `ListView.builder` + `key`.
- Jangan simpan controller di `build()` — simpan di `State` dan dispose.
- Jangan lakukan network call di `build()` — pindah ke `initState` atau `didChangeDependencies`.
- Gunakan `const` constructor untuk widget yang immutable.
## 7. Git & Commit
### 7.1 Commit Message
- Format: `<type>(<scope>): <description>`
- Type: `feat`, `fix`, `refactor`, `style`, `docs`, `test`, `chore`
- Contoh: `feat(kasir): tambah tombol Tambah Manual untuk pesanan offline`
- Jangan commit dengan message "update" atau "fix" tanpa deskripsi.
### 7.2 Branching
- Feature branch dari `main`: `feature/nama-fitur`
- Bug fix dari `main`: `fix/nama-bug`
- Jangan commit langsung ke `main` kecuali hotfix yang sudah di-review.
## 8. Anti-Patterns (Jangan Lakukan)
- ❌ Jangan hardcode warna, spacing, atau font size — gunakan design tokens.
- ❌ Jangan pakai `GlobalKey` kecuali benar-benar perlu.
- ❌ Jangan simpan state di `StatelessWidget` — pindah ke `StatefulWidget` atau `AppState`.
- ❌ Jangan lupa dispose controller/timer/stream subscription.
- ❌ Jangan paksa `setState` di dalam `build()` — akan infinite loop.
- ❌ Jangan pakai `await` di dalam `build()` tanpa FutureBuilder/future handling.
- ❌ Jangan lupa handle error untuk semua async — jangan biarkan app crash.
- ❌ Jangan gabungkan logic bisnis dengan UI — pisahkan di `AppState` atau service layer.
- ❌ Jangan duplicate widget — ekstrak jadi widget terpisah atau helper function.
- ❌ Jangan lupa check `mounted` sebelum akses context setelah async gap.
## 9. Referensi Codebase
- `lib/state/app_state.dart` — pattern state management, async handling, offline queue.
- `lib/screens/kasir_screen.dart` — pattern dialog, bottom sheet, form handling.
- `lib/screens/riwayat_screen.dart` — pattern list dengan role-based visibility, detail sheet.
- `lib/utils/colors.dart` — design tokens (warna, spacing, radius).
- `lib/utils/format.dart` — formatRupiah, formatTanggal, formatWaktu.
- `lib/models/` — pattern model dengan factory constructor + toJson/fromJson.

View File

@@ -16,50 +16,150 @@ Gunakan skill ini sebagai acuan setiap kali mengubah, menambah, atau meninjau ta
- Radius default: 18 untuk kartu, 12 untuk input/dialog, 8 untuk chip/tag.
- Font: gunakan `Theme.of(context).textTheme` agar sesuai pengaturan sistem.
### 2.2 Warna Semantik
### 2.2 Warna Semantik & Token
- `kAccent` — amber untuk aksen dan highlight.
- `kSuccess` — hijau untuk status lunas/berhasil.
- `kDanger` — merah untuk pembatalan/error/stok habis.
- `kWarning` — amber/oranye untuk peringatan stok rendah, bayar nanti.
- `kMuted` — abu-abu untuk teks sekunder/timestamp.
- `kSurface2` — background chip yang tidak aktif.
- `kBorder` — border untuk chip dan outline.
- `kText` — warna teks utama (default di theme gelap).
### 2.3 Spacing & Layout
- Gunakan `EdgeInsets` tetap untuk konsistensi:
- `EdgeInsets.all(12)` padding screen/list.
- `EdgeInsets.all(16)` padding form/dialog.
- `EdgeInsets.symmetric(horizontal: 14, vertical: 10)` untuk baris list.
- Gap antar field/form: `SizedBox(height: 10)`.
- Gap antar section: `SizedBox(height: 14)`.
### 2.3 Typography Scale
Gunakan scale berikut, jangan hardcode font size kecuali specified:
- **Display / H1**: 28px, bold, untuk heading besar
- **H2**: 22px, bold, untuk section title
- **H3**: 18px, semi-bold, untuk card title / dialog title
- **Body large**: 16px, regular, untuk teks isi utama
- **Body**: 14px, regular, untuk teks body standar
- **Body small**: 13px, regular, untuk helper/subtitle
- **Caption**: 12px, regular, untuk timestamp/label kecil
- **Overline**: 10px, bold, uppercase tracking, untuk badge/status kecil
- **Monospace**: untuk angka/harga agar stabil
### 2.4 Kartu (Card)
- Semua item list/kartu harus punya padding minimal `EdgeInsets.all(12)`.
- Jangan hardcode tinggi kartu; gunakan `Column` + `MainAxisSize.min` atau `IntrinsicHeight` bila perlu.
- Jika ada 3+ field di dalam kartu, prioritaskan wrap/scroll.
- Status transaksi harus jelas:
- Lunas: badge/icon hijau.
- Belum lunas: badge/icon amber/oranye.
- Dibatalkan: badge/icon merah + `TextDecoration.lineThrough`.
Semua angka harga/uang **wajib** menggunakan `formatRupiah()`.
### 2.5 Input & Form
- Label jelas, helper text opsional untuk field opsional.
- Jangan taruh form di dalam `Column` tanpa `SingleChildScrollView` jika total tinggi form bisa melebihi layar.
- Saat keyboard muncul, pastikan field terlihat:
- Bungkus form dengan `SingleChildScrollView` + `padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom)`.
### 2.4 Spacing Scale
Pakai multiples of 4 untuk spacing:
- 4, 8, 12, 16, 20, 24, 32, 48
- Padding screen/list: `EdgeInsets.all(12)`
- Padding form/dialog: `EdgeInsets.all(16)`
- Gap antar field: `SizedBox(height: 10)` atau `SizedBox(height: 12)`
- Gap antar section: `SizedBox(height: 14)` atau `SizedBox(height: 20)`
- Jangan gunakan nilai spacing yang tidak kelipatan 4 kecuali ada alasan khusus
### 2.6 Tombol & Aksi
- Primary action (Bayar, Tambah, Simpan): `FilledButton` berwarna aksen.
- Destructive action (Hapus, Batalkan): `FilledButton` / `OutlinedButton` berwarna merah (`kDanger`).
- Secondary action (Batal, Tutup): `TextButton` atau `OutlinedButton`.
- Setiap dialog harus punya urutan aksi: **Konfirmasi** di kanan/bawah, **Batal** di kiri/atas.
### 2.5 Elevation & Surface
- Card: `Card` widget dengan default elevation, atau `Container` dengan `color: kSurface2` jika butuh kontrol penuh.
- Active/pressed state: gunakan `InkWell` / `InkResponse` dengan splash color aksen transparan.
- Bottom sheet: `color: kSurface`, border top `kBorder`, radius top 24.
- Dialog: `backgroundColor: kSurface`, content padding dari `AlertDialog` default atau override sesuai kebutuhan.
### 2.6a Haptic & Feedback
### 2.6 Iconography
- Ukuran icon default: 20–24dp untuk list tile, 18dp untuk button, 28dp untuk dialog icon.
- Gunakan outlined variant untuk icon di dalam input/field (prefixIcon).
- Gunakan filled/rounded untuk icon utama dan CTA.
- Jangan campur gaya icon dalam satu screen.
### 2.7 Animation Standard
- **Micro-interaction** (button press, card tap): duration 150–200ms, curve `Curves.easeInOut`
- **Sheet expand/collapse**: duration 300ms, curve `Curves.fastOutSlowIn`
- **Dialog enter/exit**: duration 200ms, curve `Curves.easeInOut`
- **Spinner**: `CircularProgressIndicator` dengan `strokeWidth: 2`, duration default
- **Pulse animation** (cart badge): duration 180ms, scale 1.0 → 0.9 → 1.0
- **SnackBar**: duration default 3 detik, kecuali butuh lebih pendek/lama
- Jangan animasikan opacity/color untuk status penting (sukses/gagal) — gunakan warna solid agar cepat terdeteksi
### 2.8 Haptic & Feedback
- Aksi signifikan yang cepat: tambah item ke keranjang, print struk, scan barcode → `HapticFeedback.lightImpact()`.
- Aksi destruktif atau butuh konfirmasi: hapus item/batal transaksi → `HapticFeedback.mediumImpact()`.
- Sukses transaksi/tampilkan feedback →lanjut ke `SnackBar` hijau (`kSuccess`), jangan pakai dialog sukses untuk hal yang cepat.
- Jangan paksa haptic di setiap tap; hanya untuk aksi yang jelas berdampak.
### 2.7 Auto-Scroll & Keyboard Behavior
### 2.9 Loading State
- **Inline di button** (recommended untuk semua operasi > 500ms):
- Spinner: `SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF06210F)))` untuk light button text
- Spinner: `SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: kAccent))` untuk dark button text
- Ganti label menjadi `Mencetak...` / `Memproses...` / `Menyimpan...`
- Disable button (`onPressed: submitting ? null : ()`)
- **Full-screen**: gunakan `CircularProgressIndicator(color: kAccent)` di `Center`.
- **Inline di list/sheet**: gunakan `LinearProgressIndicator` hanya untuk determinate progress yang jelas.
- **Skeleton**: untuk list yang loading, pakai `ListTile` dengan `shimmer effect` atau placeholder abu-abu.
- Jangan biarkan user bisa double-tap aksi yang sudah loading.
### 2.10 Empty State
- Jangan tampilkan teks kosong tanpa visual indicator.
- Minimal: icon 48dp + teks. Contoh: `Icon(Icons.shopping_cart_outlined, size: 48, color: kMuted)` + teks "Keranjang kosong".
- Center alignment, `Column` dengan `MainAxisSize.min`.
- Jika ada aksi yang bisa dilakukan (misal tambah produk), tampilkan `FilledButton` sebagai helper.
- Konsisten pakai ikon `outlined` variant untuk empty state.
### 2.11 Error State
- Error dari form validation: tampilkan inline di bawah field, warna `kDanger`, font 13, `maxLines: 2`.
- Error dari operasi database/API: tampilkan `SnackBar` merah (`kDanger`) dengan durasi 3 detik.
- Jangan hanya pakai warna tanpa label. Tambahkan ikon/prefix jika perlu.
- Jika field error, pastikan `TextField` tetap bisa di-focus dan di-scroll.
- Untuk error yang bisa diperbaiki (misal form), jangan matikan form.
### 2.12 Dialog Button Layout
- **Mobile (< 600dp)**: tombol dialog harus **full-width stacked vertikal** dengan `SizedBox(height: 8)` antar tombol.
- **Tablet (>= 600dp)**: bisa pakai `Row` horizontal untuk 2 tombol, dengan `Expanded`.
- Urutan: **Batal** di atas/kiri, **Konfirmasi** di bawah/kanan.
- Tombol konfirmasi destruktif: `FilledButton` dengan `backgroundColor: kDanger`.
- Tombol konfirmasi positif: `FilledButton` dengan `backgroundColor: kAccent` atau `kSuccess`.
- Gunakan `StatefulBuilder` jika dialog butuh loading state atau update UI setelah aksi.
### 2.13 Role-Based Visibility
- Fitur yang tidak diizinkan untuk role tertentu harus **disembunyikan sepenuhnya**, bukan cuma di-disable.
- Admin: lihat semua transaksi + filter tanggal + aksi batalkan transaksi + pengaturan toko + kelola akun kasir + harga pokok/laba.
- Kasir: lihat transaksi hari ini, tidak ada aksi batalkan, tidak ada filter tanggal, tidak ada pengaturan toko, tidak ada harga pokok/laba.
- Guest: tidak diizinkan akses.
### 2.14 Bottom Sheet
- Keranjang harus berupa bottom sheet, bukan panel tetap.
- Sheet harus bisa di-drag (`DraggableScrollableSheet` atau `showModalBottomSheet` dengan `isScrollControlled: true`).
- Konten sheet harus **selalu scrollable** (`SingleChildScrollView`), tanpa `Expanded` di dalamnya, agar footer tidak pernah terhalangi keyboard.
- Untuk menutup sheet dari aksi di dalamnya, gunakan `Navigator.pop(sheetContext)` bukan `parentContext`.
- Tambah handle bar (container 40x4 dengan radius 4, warna `kBorder`) di atas sheet untuk affordance drag.
### 2.15 Dialog
- Semua dialog input yang panjang harus dibungkus `SingleChildScrollView` atau `SingleChildScrollView` di dalam `AlertDialog` agar bisa di-scroll saat keyboard muncul.
- Jangan pakai `Expanded` di dalam dialog yang berpotensi overflow.
- Dialog dengan form: `contentPadding: EdgeInsets.fromLTRB(16, 16, 16, 8)`, `actionsPadding: EdgeInsets.fromLTRB(16, 0, 16, 16)`.
- Dialog dengan list items: `contentPadding: EdgeInsets.all(16)`.
- Jangan buat dialog terlalu tinggi; batasi dengan `ConstrainedBox` + `SingleChildScrollView` jika konten bisa panjang.
### 2.16 SnackBar
- Gunakan `ScaffoldMessenger.of(context).showSnackBar()` untuk feedback singkat.
- Durasi default 3 detik untuk info, 5 detik untuk error penting.
- Posisikan di atas bottom navigation jika ada, atau default bottom.
- Jangan tampilkan SnackBar di atas dialog yang masih terbuka — tunggu dialog ditutup dulu.
### 2.17 Status & Badge
- Gunakan badge untuk status dengan warna sesuai:
- Lunas / Berhasil: badge hijau (`kSuccess`) dengan label teks
- Belum Lunas / Bayar Nanti: badge amber (`kWarning`) dengan label teks
- Dibatalkan: badge merah (`kDanger`) + teks `DIBATALKAN`
- Stok Habis: badge merah + teks "Habis"
- Stok Rendah: badge oranye + teks "Stok rendah (X/Y)"
- Manual: badge amber dengan label "MANUAL" (caps, tracking +0.5)
- Badge style: `Container` dengan `padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2)`, `borderRadius: BorderRadius.circular(8)`, `color: warna.withValues(alpha: 0.15)`.
- Untuk badge kecil (10–12px), gunakan `fontWeight: FontWeight.w800` agar tetap terbaca.
### 2.18 List & Card
- Gunakan `ListView.separated` dengan `itemCount` yang sesuai untuk performa.
- Separator: `Divider(height: 1, thickness: 1, color: kBorder)` atau `SizedBox(height: 8/12)` untuk card list.
- Card list: `Card` widget dengan `margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4)` atau `EdgeInsets.all(12)`.
- Jangan hardcode tinggi list item; gunakan `Column` + `MainAxisSize.min`.
- Teks panjang: `maxLines: 2`, `overflow: TextOverflow.ellipsis` untuk nama produk.
### 2.19 Grid Produk
- Default: 2 kolom di layar HP (`crossAxisCount: 2`).
- Tablet (lebar > 600dp): naik jadi 3 kolom.
- Jangan pakai `childAspectRatio` tetap yang bikin kartu terlalu tinggi/pendek. Gunakan `AspectRatio(1.1)` atau biarkan `GridView` yang menentukan tinggi.
- Setiap kartu produk harus menampilkan: inisial nama (avatar circle amber), nama produk, harga, stok/badge stok.
### 2.20 Auto-Scroll & Keyboard
- Setiap form di dalam bottom sheet atau dialog harus meng-handle keyboard muncul/tutup.
- Gunakan `MediaQuery.of(context).viewInsets.bottom` untuk padding bawah dinamis pada `SingleChildScrollView`.
- Untuk bottom sheet form yang tinggi dan perlu auto-expand:
@@ -69,69 +169,20 @@ Gunakan skill ini sebagai acuan setiap kali mengubah, menambah, atau meninjau ta
4. Durasi animasi sheet: `300ms`, scroll: `250ms`, alignment: `0.0`.
- Jangan andal `Scaffold.resizeToAvoidBottomInset` saja untuk bottom sheet form kompleks; manual padding lebih stabil untuk kasus form panjang.
### 2.8 Empty State
- Jangan tampilkan teks kosong tanpa visual indicator.
- Minimal: icon + teks. Contoh: `Icon(Icons.shopping_cart_outlined, size: 48, color: kMuted)` + teks "Keranjang kosong".
- Center alignment, `Column` dengan `MainAxisSize.min`.
- Konsisten pakai ikon `outlined` variant untuk empty state.
### 2.21 Responsif
- Mobile (< 600dp): 2 kolom grid, tombol stacked vertikal di dialog.
- Tablet (>= 600dp): 3–4 kolom grid, tombol bisa horizontal di dialog.
- Gunakan `LayoutBuilder` atau `MediaQuery` untuk menentukan layout berdasarkan lebar screen.
- Jangan gunakan hardcoded width untuk widget utama; gunakan `Expanded`, `Flexible`, atau `MediaQuery`.
### 2.9 Dialog Button Layout
- Mobile: tombol dialog harus **full-width stacked vertikal** dengan `SizedBox(height: 8)` antar tombol.
- Urutan: **Batal** di atas, **Konfirmasi** di bawah.
- Jangan pakai `Row` untuk 2 tombol di dialog mobile, kecuali screen width > 600dp.
- Tombol konfirmasi destruktif: `FilledButton` dengan `backgroundColor: kDanger`.
- Tombol konfirmasi positif: `FilledButton` dengan `backgroundColor: kAccent` atau `kSuccess`.
### 2.22 Aksesibilitas
- Minimal tap target: 44x44dp untuk semua tombol/icon.
- Kontras teks minimal 4.5:1 (cek dengan tool contrast checker).
- Jangan andal warna saja (tambah ikon/label). Contoh: stok habis = badge merah + teks "Habis".
- Setiap aksi ada `SnackBar` feedback: sukses hijau, error merah, info biru/abu.
- Gunakan `Semantics` widget untuk icon-only button agar screen reader bisa membaca.
### 2.10 Loading State
- Untuk operasi < 500ms: tidak perlu loading indicator.
- Untuk operasi > 500ms: tampilkan loading di button yang dipencet.
- Spinner: `CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF06210F))` untuk light button text.
- Spinner: `CircularProgressIndicator(strokeWidth: 2, color: kAccent)` untuk dark button text.
- Untuk full-screen loading: gunakan `CircularProgressIndicator(color: kAccent)` di `Center`.
- Jangan gunakan `LinearProgressIndicator` kecuali untuk determinate progress yang jelas.
- Jika list kosong dan pengguna bisa melakukan aksi (misal tambah produk), tampilkan `FloatingActionButton` atau `FilledButton` sebagai helper agar tidak dead-end.
### 2.11 Error State
- Error dari form validation: tampilkan inline di bawah field, warna `kDanger`, font 13.
- Error dari operasi database/API: tampilkan `SnackBar` merah (`kDanger`).
- Jangan hanya pakai warna tanpa label. Tambahkan ikon/prefix jika perlu.
- Jika field error, pastikan `TextField` tetap bisa di-focus dan di-scroll.
## 3. Responsif & Overflow
### 3.1 Grid Produk (Kasir)
- Default: 2 kolom di layar HP (`crossAxisCount: 2`).
- Tablet (lebar > 600dp): naik jadi 3 kolom.
- Jangan pakai `childAspectRatio` tetap yang bikin kartu terlalu tinggi/pendek. Gunakan `AspectRatio(1.1)` atau biarkan `GridView` yang menentukan tinggi.
### 3.2 Payment Method Chips
- Urutan chip di UI: **Tunai**, **Bank**, **QRIS**, **Bayar Nanti**, **Split**.
- Untuk mobile, kelompokkan menjadi:
- Baris atas: 3 chip (`Tunai`, `Bank`, `QRIS`) dalam satu `Row` dengan `Expanded`.
- Divider tipis antar kelompok.
- Baris bawah: 2 chip (`Bayar Nanti`, `Split`) dalam satu `Row` dengan `Expanded`.
- Jangan pakai `Wrap` untuk payment chips. `Wrap` boleh dipakai kembali untuk kasus lain seperti `Filter chips` atau `Tag`.
- Chip harus compact, aman di layar kecil, dan tidak memaksa scroll horizontal.
### 3.3 Bottom Sheet
- Keranjang harus berupa bottom sheet, bukan panel tetap.
- Sheet harus bisa di-drag (`DraggableScrollableSheet` atau `showModalBottomSheet` dengan `isScrollControlled: true`).
- Konten sheet harus **selalu scrollable** (`SingleChildScrollView`), tanpa `Expanded` di dalamnya, agar footer tidak pernah terhalangi keyboard.
- Untuk menutup sheet dari aksi di dalamnya, gunakan `Navigator.pop(sheetContext)` bukan `parentContext`.
### 3.4 Dialog
- Semua dialog input yang panjang harus dibungkus `SingleChildScrollView` atau `SingleChildScrollView` di dalam `AlertDialog` agar bisa di-scroll saat keyboard muncul.
- Jangan pakai `Expanded` di dalam dialog yang berpotensi overflow.
### 3.5 Produk & Form Panjang
- Tab Produk adalah gabungan form + list produk. Gunakan `SingleChildScrollView` + `ListView.builder` dengan `shrinkWrap: true` agar form bisa di-scroll tanpa overflow.
### 3.6 Teks
- Jangan hardcode font size. Gunakan skema `TextStyle` dari `ThemeData`.
- Jika label teks bisa panjang (mis. nama produk), batasi maksimal 2 baris dengan `maxLines: 2` + `overflow: TextOverflow.ellipsis`.
- ID transaksi/nama panjang: `Expanded` + `TextOverflow.ellipsis` agar tidak overflow di `Row`.
## 4. UX untuk Kasir (Critical Path)
## 3. UX untuk Kasir (Critical Path)
- Target: transaksi selesai dalam 3 tap (pilih produk → bayar → selesai).
- Jangan sembunyikan aksi utama (Bayar) di bawah banyak opsi.
- Tampilkan total, diskon, dan kembalian secara **real-time**.
@@ -139,48 +190,22 @@ Gunakan skill ini sebagai acuan setiap kali mengubah, menambah, atau meninjau ta
- Setiap item di keranjang harus bisa diubah qty atau tipe harga dalam 1 tap.
- Jika stok habis, matikan visual (opacity turun, disable tap) dan tampilkan badge "Habis".
- Jika stok menipis, tampilkan badge peringatan oranye sebelum habis.
- Bottom bar keranjang harus selalu terlihat, tidak tertutup keyboard.
## 5. Payment Flow: Bayar Nanti
## 4. Payment Flow: Bayar Nanti
- Saat checkout pilih **Bayar Nanti**, tombol harus jelas: **"Pesanan Sekarang"**.
- Field "Uang Dibayar" menjadi disabled dengan label **"Belum Bayar"**.
- Tampilkan status **"Belum Bayar"** di receipt/summary dengan warna `kMuted`.
- Tampilkan status **"Belum Bayar"** di receipt/summary dengan warna `kWarning`.
- Di Riwayat, transaksi unpaid harus punya badge/status yang menonjol.
- Saat customer datang bayar: tombol **"Terima Pembayaran"** atau **"Bayar Sisa"** di detail transaksi.
## 6. Aksesibilitas & Feedback
- Minimal tap target: 44x44dp untuk semua tombol/icon.
- Warna teks harus kontras dengan background (minimal 4.5:1 untuk teks normal).
- Jangan hanya andal warna untuk status (tambah ikon/label). Contoh: stok habis = badge merah + teks "Habis".
- Setiap aksi berhasil/gagal harus ada `SnackBar` feedback:
- Sukses: hijau (`kSuccess`).
- Error: merah (`kDanger`).
- Info: biru/abu.
- Loading state harus muncul saat operasi > 500ms. Gunakan `CircularProgressIndicator(color: kAccent)`.
## 7. Role-Based Visibility
- Fitur yang tidak diizinkan untuk role tertentu harus **disembunyikan sepenuhnya**, bukan cuma di-disable.
- Admin: lihat semua transaksi + filter tanggal + aksi batalkan transaksi.
- Kasir: lihat transaksi hari ini, tidak ada aksi batalkan, tidak ada filter tanggal.
## 8. List & Pagination
- Gunakan `ListView.separated` dengan `itemCount` yang sesuai untuk performa.
- Tombol "Muat lebih banyak" harus ada di bawah list jika data bisa banyak.
- Filter chips: gunakan `Wrap` agar responsif di layar kecil.
## 8a. Printer & Thermal Receipt
- Semua teks struk harus Bahasa Indonesia dan ringkas.
- Struk wajib menampilkan: nama toko, tanggal, jam, nomor transaksi, daftar item, diskon (jika ada), total, metode pembayaran, detail pembayaran tunai/split (jika ada), dan **nama kasir**.
- Jangan tampilkan label per-item **Umum/Reseller** di struk thermal, agar lebih rapi.
- Jika `splitPayment` ada, tampilkan per bagian: `Split Tunai`, `Split Bank`, atau `Split QRIS` beserta nominalnya.
- Batasi lebar konten sesuai PaperSize.mm58 agar tidak terpotong.
- Setelah cetak, arahkan feedback ke user melalui `SnackBar`, bukan hanya log.
- Flow pairing printer harus bisa diakses oleh role kasir, bukan hanya admin.
## 9. Reusability
- Buat widget kecil reuseable untuk komponen yang berulang: `_Tag`, `_ReportCard`, `_Avatar`, `_Stepper`, `_PaymentMethodChip`.
## 5. Reusability & Konsistensi
- Buat widget reuseable untuk komponen berulang: `_Tag`, `_ReportCard`, `_Avatar`, `_Stepper`, `_PaymentMethodChip`, `_ReceiptRow`, `_SettingsListTile`.
- Jangan duplikasi warna/spacing. Taruh konstanta di `lib/utils/colors.dart`.
- Konsisten pakai `formatRupiah()` untuk semua angka mata uang.
- Nama widget internal: pakai underscore prefix (`_`) kecuali widget yang diekspor.
## 10. Checklist Sebelum Submit Perubahan UI/UX
## 6. Checklist Sebelum Submit Perubahan UI/UX
- [ ] `flutter analyze` → No issues found (tanpa error).
- [ ] Coba di layar kecil (HP 5") dan besar (tablet).
- [ ] Coba dengan keyboard muncul (input uang/diskon).
@@ -188,8 +213,12 @@ Gunakan skill ini sebagai acuan setiap kali mengubah, menambah, atau meninjau ta
- [ ] Pastikan tidak ada hardcoded height yang bikin overflow.
- [ ] Semua teks dalam Bahasa Indonesia.
- [ ] Konsisten pakai `formatRupiah()` untuk angka mata uang.
- [ ] Coba flow lengkap: tambah produk → checkout → cetak struk → riwayat → detail.
- [ ] Coba role kasir dan admin untuk memastikan visibility benar.
## 11. Referensi Project
## 7. Referensi Project
- `lib/utils/colors.dart` — design tokens.
- `lib/screens/kasir_screen.dart` — contoh bottom sheet cart, grid produk, dialog pembayaran.
- `lib/screens/riwayat_screen.dart` — contoh kartu laporan, filter role-based.
- `lib/screens/pengaturan_screen.dart` — contoh settings form + role-based visibility.