Files
app-grosir/.kilo/agent/uiux-pos-grosir.md

225 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI/UX Design Skill — Dutra Store (POS Grosir Flutter)
Gunakan skill ini sebagai acuan setiap kali mengubah, menambah, atau meninjau tampilan aplikasi POS grosir berbasis Flutter ini. Jika ada pertentangan, prioritaskan yang tercantum di sini.
## 1. Tujuan Utama
- Transaksi cepat tanpa hambatan visual.
- Tidak ada overflow/keluar layar di ukuran HP kecil.
- Status stok, harga, dan aksi utama terlihat jelas dalam 1 scan.
- Maintain konsistensi visual antar screen.
## 2. Design System
### 2.1 Tema
- Theme gelap (dark) sebagai basis utama.
- Warna aksen: amber (`Colors.amber`).
- 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 & 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 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
Semua angka harga/uang **wajib** menggunakan `formatRupiah()`.
### 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.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.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.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:
1. Pakai `DraggableScrollableController`.
2. Buat `FocusNode` untuk setiap `TextField`.
3. Tambah listener ke setiap focus node: jika `hasFocus`, animasikan sheet ke `maxChildSize` (misal `0.92`), lalu panggil `Scrollable.ensureVisible()` agar field aktif terlihat.
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.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.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.
## 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**.
- Gunakan chip/segmented button untuk tipe pembeli (Umum/Reseller) — jangan dropdown.
- 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.
## 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 `kWarning`.
- Di Riwayat, transaksi unpaid harus punya badge/status yang menonjol.
- Saat customer datang bayar: tombol **"Terima Pembayaran"** atau **"Bayar Sisa"** di detail transaksi.
## 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.
## 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).
- [ ] Coba mode gelap dan terang (jika supported).
- [ ] 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.
## 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.