196 lines
11 KiB
Markdown
196 lines
11 KiB
Markdown
# 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
|
|
- `kAccent` — amber untuk aksen dan highlight.
|
|
- `kSuccess` — hijau untuk status lunas/berhasil.
|
|
- `kDanger` — merah untuk pembatalan/error/stok habis.
|
|
- `kMuted` — abu-abu untuk teks sekunder/timestamp.
|
|
- `kSurface2` — background chip yang tidak aktif.
|
|
- `kBorder` — border untuk chip dan outline.
|
|
|
|
### 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.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`.
|
|
|
|
### 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.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.6a 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
|
|
- 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.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.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.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)
|
|
- 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.
|
|
|
|
## 5. 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`.
|
|
- 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`.
|
|
- Jangan duplikasi warna/spacing. Taruh konstanta di `lib/utils/colors.dart`.
|
|
|
|
## 10. 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.
|
|
|
|
## 11. 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.
|