Bersihkan 18 file sampah AI + aturan gitignore file bantu AI (taruh di tmp/)
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -37,6 +37,17 @@ migrate_working_dir/
|
|||||||
lib/config.dart
|
lib/config.dart
|
||||||
supabse.txt
|
supabse.txt
|
||||||
|
|
||||||
|
# File bantu AI (script cek/debug sekali pakai) — JANGAN commit
|
||||||
|
# Taruh di D:\HomeLabs\tmp\ (tidak masuk repo manapun).
|
||||||
|
# Dilarang menaruh *.py / check_* / fix_* / test_syntax* / *.bak / *.tmp
|
||||||
|
# di root project atau lib/.
|
||||||
|
*_ai_helper.py
|
||||||
|
check_*.py
|
||||||
|
fix_*.py
|
||||||
|
test_syntax*.py
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
|
|||||||
38
PRD.md
38
PRD.md
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v1.3.0+6 — 2026-10-18
|
||||||
|
- **Kasir list + toggle grid/list:** default list (foto 56px), toggle ke grid. Tap = tambah qty berkali-kali. Highlight hijau sesaat (0.9s). Tombol keranjang + badge jumlah item.
|
||||||
|
- **Notifikasi in-app:** banner SnackBar + getar — hanya untuk admin, hanya untuk transaksi dari orang lain (bukan transaksi admin sendiri). Deteksi via polling (interval 1 menit) + stream `newSaleNotifications`. Load pertama dilewati agar tidak spam.
|
||||||
|
- **Badge Riwayat:** `unseenSalesCount` di tab Riwayat (admin only), reset saat tab dibuka (`markSalesSeen()`).
|
||||||
|
- **Fix tombol "Muat lebih banyak" Riwayat:** cek `filteredSales.length` bukan `state.sales` (sebelumnya negatif -17 saat filter aktif).
|
||||||
|
- Fix Gitea ROOT_URL (infra): link download APK sebelumnya mengarah ke IP Tailscale lama → fixed ke `https://git.gosambi.uk/`.
|
||||||
|
|
||||||
|
### v1.2.0+5 — 2026-10-17
|
||||||
|
- **Foto produk:** bucket Supabase Storage `product-images` (publik, limit 5MB, jpg/png/webp) + kolom `products.image_path`. `image_picker` ^1.2.0 (kompres 800px/quality80). Tombol Kamera/Galeri/Hapus di form Tambah+Edit (`produk_screen.dart`). Thumbnail di daftar produk (`_ProductRow`) + keranjang kasir (`_KasirAvatar`). AndroidManifest +queries intent kamera/galeri.
|
||||||
|
- **Fix MIME:** `image/jpg` → `image/jpeg` (server 415).
|
||||||
|
- **Fix storage policy:** bucket `storage.objects` tanpa policy → upload 403. Fix: 3 policy SQL (INSERT auth, SELECT public, DELETE auth) — fix di server, APK tidak perlu rebuild.
|
||||||
|
|
||||||
### v1.1.0 — 2026-07-24
|
### v1.1.0 — 2026-07-24
|
||||||
- Dialog pembayaran kasir: tombol "Selesai & Cetak" menampilkan loading state (spinner + "Memproses...") dengan guard anti-doble-klik.
|
- Dialog pembayaran kasir: tombol "Selesai & Cetak" menampilkan loading state (spinner + "Memproses...") dengan guard anti-doble-klik.
|
||||||
- Dialog struk pembayaran: tombol "Cetak Struk" memiliki loading state ("Mencetak...") dan disable saat proses cetak.
|
- Dialog struk pembayaran: tombol "Cetak Struk" memiliki loading state ("Mencetak...") dan disable saat proses cetak.
|
||||||
@@ -493,3 +505,29 @@ Dutra Store adalah aplikasi kasir digital berbasis Flutter untuk toko grosir. Ap
|
|||||||
4. Jalankan SQL tambahan untuk kolom `store_id` (multi-tenant preparation): tambah tabel `stores`, tambah kolom `store_id` ke `products`, `sales`, `store_settings`, backfill data existing dengan default store ID, dan buat index. RLS dimatikan untuk semua tabel.
|
4. Jalankan SQL tambahan untuk kolom `store_id` (multi-tenant preparation): tambah tabel `stores`, tambah kolom `store_id` ke `products`, `sales`, `store_settings`, backfill data existing dengan default store ID, dan buat index. RLS dimatikan untuk semua tabel.
|
||||||
- UI/UX skill tersimpan di `.kilo/agent/uiux-pos-grosir.md` dan bagian PRD 10.1-10.7.
|
- UI/UX skill tersimpan di `.kilo/agent/uiux-pos-grosir.md` dan bagian PRD 10.1-10.7.
|
||||||
- Color scheme: brand amber dominant di Kasir & Produk, hijau untuk harga umum, merah untuk error/habis, abu untuk muted.
|
- Color scheme: brand amber dominant di Kasir & Produk, hijau untuk harga umum, merah untuk error/habis, abu untuk muted.
|
||||||
|
|
||||||
|
## Infra & Setup App
|
||||||
|
|
||||||
|
### Backend Supabase (Self-Host)
|
||||||
|
- URL: `https://supa.gosambi.uk` (gateway envoy port 8001)
|
||||||
|
- Keys: `D:\HomeLabs\backups\supabase-keys-local.txt` (ANON + SERVICE)
|
||||||
|
- Backup data: script `D:\HomeLabs\scripts\supabase_backup.py` → `D:\HomeLabs\backups\supabase-nwuupvnodsgxdhqcakqr\`
|
||||||
|
- Schema backup: `backups\...\schema_selfhost.sql`
|
||||||
|
- Config app: `lib\config.dart` (gitignored — berisi URL + keys, jangan commit)
|
||||||
|
- Migrasi cloud → self-host selesai 2026-10-17
|
||||||
|
- Akun: admin@posgrosir.local, kasir1@toko.com, bibi/bubu/baba@toko.com (password sementara `GrosirBaru2026!`)
|
||||||
|
|
||||||
|
### Standar Rilis APK
|
||||||
|
1. Naikkan versi di `pubspec.yaml` (misal `1.2.0+5` → `1.3.0+6`).
|
||||||
|
2. Commit & `git push gitea main` (single remote Gitea; jangan commit APK ke repo).
|
||||||
|
3. `flutter build apk --release` di `D:\HomeLabs\app-grosir` (jika gagal: `.gradlew --stop` + `flutter clean`).
|
||||||
|
4. Verifikasi APK: file `build\app\outputs\flutter-apk\app-release.apk`; pastikan string `supa.gosambi.uk` ada di `libapp.so`.
|
||||||
|
5. Instal ke HP: `adb -s fd683806 install -r app-release.apk`.
|
||||||
|
6. Buat tag `v1.x.y` di Gitea + upload APK ke **Release** (`git.gosambi.uk/onie/app-grosir/releases`), jangan upload APK langsung ke repo Git.
|
||||||
|
7. Catat versi baru di PRD.md ini + rule.md (singkat saja).
|
||||||
|
|
||||||
|
### Device Testing
|
||||||
|
- HP utama: **Realme RMX2061** (Android 11, device ID `fd683806`, package `com.grosir.app_grosir`)
|
||||||
|
- HP admin kedua: sudah ada (untuk testing notifikasi multi-device)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
stack = []
|
|
||||||
for i in range(179, 448):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for j, c in enumerate(line):
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
stack.append((i+1, j+1))
|
|
||||||
elif c == ')':
|
|
||||||
if stack:
|
|
||||||
stack.pop()
|
|
||||||
else:
|
|
||||||
print(f'Unexpected ) at line {i+1}:{j+1}')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if stack:
|
|
||||||
print(f'Unclosed ( at:')
|
|
||||||
for item in stack:
|
|
||||||
print(f' Line {item[0]}:{item[1]}')
|
|
||||||
else:
|
|
||||||
print('All balanced!')
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Check bracket balance for _showAddForm (lines 180-431)
|
|
||||||
stack = []
|
|
||||||
for i in range(179, 431):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for j, c in enumerate(line):
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
stack.append((i+1, j+1, line.rstrip()))
|
|
||||||
elif c == ')':
|
|
||||||
if stack:
|
|
||||||
stack.pop()
|
|
||||||
else:
|
|
||||||
print(f'Unexpected ) at line {i+1}:{j+1}')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if stack:
|
|
||||||
print(f'Unclosed ( at:')
|
|
||||||
for item in stack:
|
|
||||||
print(f' Line {item[0]}:{item[1]} | {item[2]}')
|
|
||||||
else:
|
|
||||||
print('All balanced!')
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
opens = 0
|
|
||||||
closes = 0
|
|
||||||
for i in range(179, 431):
|
|
||||||
line = lines[i]
|
|
||||||
o = line.count('(')
|
|
||||||
c = line.count(')')
|
|
||||||
opens += o
|
|
||||||
closes += c
|
|
||||||
if o != 0 or c != 0:
|
|
||||||
print(f'{i+1}: +{o}-{c} = {opens-closes} | {line.rstrip()}')
|
|
||||||
|
|
||||||
print(f'\nTotal (: {opens}, ): {closes}, diff: {opens-closes}')
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
opens = 0
|
|
||||||
closes = 0
|
|
||||||
for i in range(179, 436):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for c in line:
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
opens += 1
|
|
||||||
elif c == ')':
|
|
||||||
closes += 1
|
|
||||||
|
|
||||||
print(f'Total (: {opens}, ): {closes}, diff: {opens-closes}')
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
depth = 0
|
|
||||||
for i in range(493, 644):
|
|
||||||
line = lines[i]
|
|
||||||
o = line.count('(')
|
|
||||||
c = line.count(')')
|
|
||||||
if depth == 0 and (o != 0 or c != 0):
|
|
||||||
print(f'Depth became 0 at line {i+1}: {line.rstrip()}')
|
|
||||||
depth += o - c
|
|
||||||
if depth < 0:
|
|
||||||
print(f'Negative depth at line {i+1}: {line.rstrip()}')
|
|
||||||
break
|
|
||||||
|
|
||||||
print(f'Final depth: {depth}')
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\test_syntax2.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
depth = 0
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
opens = line.count('(')
|
|
||||||
closes = line.count(')')
|
|
||||||
if opens != 0 or closes != 0 or depth != 0:
|
|
||||||
depth += opens - closes
|
|
||||||
print(f'{i+1}: +{opens}-{closes} = {depth} | {line.rstrip()}')
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
opens = 0
|
|
||||||
closes = 0
|
|
||||||
for i in range(493, 644):
|
|
||||||
line = lines[i]
|
|
||||||
o = line.count('(')
|
|
||||||
c = line.count(')')
|
|
||||||
opens += o
|
|
||||||
closes += c
|
|
||||||
if o != 0 or c != 0:
|
|
||||||
print(f'{i+1}: +{o}-{c} = {opens-closes} | {line.rstrip()}')
|
|
||||||
|
|
||||||
print(f'Total (: {opens}, ): {closes}')
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
for i in range(493, 644):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for j, c in enumerate(line):
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
print(f'{i+1}:{j+1} OPEN | {line.rstrip()}')
|
|
||||||
elif c == ')':
|
|
||||||
print(f'{i+1}:{j+1} CLOSE | {line.rstrip()}')
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
depth = 0
|
|
||||||
for i in range(493, 644):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for c in line:
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
depth += 1
|
|
||||||
elif c == ')':
|
|
||||||
depth -= 1
|
|
||||||
if depth < 0:
|
|
||||||
print(f'Negative depth at line {i+1}: {line.rstrip()}')
|
|
||||||
break
|
|
||||||
|
|
||||||
print(f'Final depth: {depth}')
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\test_syntax2.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*'):
|
|
||||||
continue
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for j, c in enumerate(line):
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
if not in_string:
|
|
||||||
in_string = True
|
|
||||||
else:
|
|
||||||
in_string = False
|
|
||||||
if in_string:
|
|
||||||
print(f'Unclosed string at line {i+1}: {line.rstrip()}')
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
stack = []
|
|
||||||
for i in range(493, 644):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for j, c in enumerate(line):
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
stack.append((i+1, j+1, line.rstrip()))
|
|
||||||
elif c == ')':
|
|
||||||
if stack:
|
|
||||||
stack.pop()
|
|
||||||
else:
|
|
||||||
print(f'Unexpected ) at line {i+1}:{j+1}')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if stack:
|
|
||||||
print(f'Unclosed ( at:')
|
|
||||||
for item in stack:
|
|
||||||
print(f' Line {item[0]}:{item[1]} | {item[2]}')
|
|
||||||
else:
|
|
||||||
print('All balanced!')
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
stack = []
|
|
||||||
for i in range(493, 649):
|
|
||||||
line = lines[i]
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
for j, c in enumerate(line):
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
continue
|
|
||||||
if c == '\\':
|
|
||||||
escape = True
|
|
||||||
continue
|
|
||||||
if c in "'\"":
|
|
||||||
in_string = not in_string
|
|
||||||
continue
|
|
||||||
if in_string:
|
|
||||||
continue
|
|
||||||
if c == '(':
|
|
||||||
stack.append((i+1, j+1, line.rstrip()))
|
|
||||||
elif c == ')':
|
|
||||||
if stack:
|
|
||||||
stack.pop()
|
|
||||||
else:
|
|
||||||
print(f'Unexpected ) at line {i+1}:{j+1}')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if stack:
|
|
||||||
print(f'Unclosed ( at:')
|
|
||||||
for item in stack:
|
|
||||||
print(f' Line {item[0]}:{item[1]} | {item[2]}')
|
|
||||||
else:
|
|
||||||
print('All balanced!')
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Find the start and end of _showAddForm
|
|
||||||
start = None
|
|
||||||
end = None
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.strip().startswith('void _showAddForm'):
|
|
||||||
start = i
|
|
||||||
if start is not None and line.strip() == '}' and i > start + 5:
|
|
||||||
# Check if this is the end of _showAddForm
|
|
||||||
# It should be followed by empty line or another top-level declaration
|
|
||||||
if i + 1 < len(lines):
|
|
||||||
next_line = lines[i + 1].strip()
|
|
||||||
if next_line == '' or next_line.startswith('void ') or next_line.startswith('class '):
|
|
||||||
end = i
|
|
||||||
break
|
|
||||||
|
|
||||||
print(f'_showAddForm: lines {start+1} to {end+1}')
|
|
||||||
|
|
||||||
# Remove 2-space indent from _showAddForm function
|
|
||||||
for i in range(start, end + 1):
|
|
||||||
if lines[i].startswith(' '):
|
|
||||||
lines[i] = lines[i][2:]
|
|
||||||
|
|
||||||
# After _showAddForm, there might be an extra class closing }
|
|
||||||
# The class _ProdukScreenState should close right after build method
|
|
||||||
# Let's find the class closing after _showAddForm and remove it if it's extra
|
|
||||||
# After _showAddForm ends, the next line should be empty or a function/class
|
|
||||||
# If there's a } followed by empty line and then another }, remove the first }
|
|
||||||
|
|
||||||
with open('D:\\app\\ai\\app-grosir\\lib\\screens\\produk_screen.dart', 'w') as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
|
|
||||||
print('Done')
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
@@ -1,30 +0,0 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:app_grosir/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(const MyApp());
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
void test(BuildContext context) {
|
|
||||||
final _scrollController = ScrollController();
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (sheetContext) {
|
|
||||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.92 - bottomInset,
|
|
||||||
),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 12),
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: _scrollController,
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomInset),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextField(decoration: InputDecoration(labelText: 'Test 1')),
|
|
||||||
TextField(decoration: InputDecoration(labelText: 'Test 2')),
|
|
||||||
TextField(decoration: InputDecoration(labelText: 'Test 3')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
void test(BuildContext context) {
|
|
||||||
final name = TextEditingController();
|
|
||||||
final barcode = TextEditingController();
|
|
||||||
final price = TextEditingController();
|
|
||||||
final priceReseller = TextEditingController();
|
|
||||||
final stock = TextEditingController();
|
|
||||||
final minStockCtrl = TextEditingController(text: '5');
|
|
||||||
final hargaPokokCtrl = TextEditingController();
|
|
||||||
final focusNodes = <FocusNode>[
|
|
||||||
FocusNode(),
|
|
||||||
FocusNode(),
|
|
||||||
FocusNode(),
|
|
||||||
FocusNode(),
|
|
||||||
FocusNode(),
|
|
||||||
FocusNode(),
|
|
||||||
FocusNode(),
|
|
||||||
];
|
|
||||||
final _scrollTimers = <FocusNode, Timer>{};
|
|
||||||
for (final node in focusNodes) {
|
|
||||||
node.addListener(() {
|
|
||||||
if (!node.hasFocus) {
|
|
||||||
_scrollTimers[node]?.cancel();
|
|
||||||
_scrollTimers.remove(node);
|
|
||||||
} else if (context.mounted) {
|
|
||||||
_scrollTimers[node]?.cancel();
|
|
||||||
_scrollTimers[node] = Timer(const Duration(milliseconds: 350), () {
|
|
||||||
if (node.context != null && node.context!.mounted) {
|
|
||||||
Scrollable.ensureVisible(
|
|
||||||
node.context!,
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
alignment: 0.1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_scrollTimers.remove(node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final _scrollController = ScrollController();
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (sheetContext) {
|
|
||||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.92 - bottomInset,
|
|
||||||
),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
border: Border(top: BorderSide(color: Colors.grey)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 12),
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.add_box_outlined, color: Colors.amber),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text('Tambah Produk',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16, fontWeight: FontWeight.w700)),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close, color: Colors.grey),
|
|
||||||
onPressed: () => Navigator.pop(sheetContext),
|
|
||||||
tooltip: 'Tutup',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 12),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: _scrollController,
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomInset),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: barcode,
|
|
||||||
focusNode: focusNodes[0],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Barcode / No. Produk',
|
|
||||||
prefixIcon: const Icon(Icons.qr_code_2_outlined),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: const Icon(Icons.qr_code_scanner_outlined, color: Colors.amber),
|
|
||||||
onPressed: () async {
|
|
||||||
final code = await Navigator.push<String>(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => const Scaffold(body: Center(child: Text('Scan'))),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (code != null && context.mounted) {
|
|
||||||
barcode.text = code;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: 'Scan barcode',
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextField(
|
|
||||||
controller: name,
|
|
||||||
focusNode: focusNodes[1],
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Nama produk',
|
|
||||||
prefixIcon: Icon(Icons.label_outline)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: price,
|
|
||||||
focusNode: focusNodes[2],
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Harga Umum',
|
|
||||||
prefixText: 'Rp ',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: priceReseller,
|
|
||||||
focusNode: focusNodes[3],
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Harga Reseller',
|
|
||||||
prefixText: 'Rp ',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextField(
|
|
||||||
controller: stock,
|
|
||||||
focusNode: focusNodes[4],
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Stok awal',
|
|
||||||
prefixIcon: Icon(Icons.numbers),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextField(
|
|
||||||
controller: hargaPokokCtrl,
|
|
||||||
focusNode: focusNodes[5],
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Harga Pokok (modal)',
|
|
||||||
prefixText: 'Rp ',
|
|
||||||
helperText: 'Kosongkan jika tidak diketahui',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextField(
|
|
||||||
controller: minStockCtrl,
|
|
||||||
focusNode: focusNodes[6],
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Stok minimum (warning)',
|
|
||||||
prefixIcon: Icon(Icons.warning_outlined),
|
|
||||||
helperText: 'Default: 5',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
final n = name.text.trim();
|
|
||||||
final bc = barcode.text.trim();
|
|
||||||
final pr = (price.text.isEmpty ? 0 : double.tryParse(price.text) ?? 0);
|
|
||||||
final prR = (priceReseller.text.isEmpty ? pr : double.tryParse(priceReseller.text) ?? pr);
|
|
||||||
final st = int.tryParse(stock.text) ?? 0;
|
|
||||||
final ms = int.tryParse(minStockCtrl.text) ?? 5;
|
|
||||||
final hp = (hargaPokokCtrl.text.isEmpty ? 0 : double.tryParse(hargaPokokCtrl.text) ?? 0);
|
|
||||||
if (n.isEmpty || pr <= 0) return;
|
|
||||||
final err = 'OK'; // await state.addProduct(n, bc, pr, prR, st, ms, hp);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
if (err == null) {
|
|
||||||
name.clear();
|
|
||||||
barcode.clear();
|
|
||||||
price.clear();
|
|
||||||
priceReseller.clear();
|
|
||||||
stock.clear();
|
|
||||||
minStockCtrl.clear();
|
|
||||||
hargaPokokCtrl.clear();
|
|
||||||
messenger.showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Produk berhasil ditambahkan'),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.pop(sheetContext);
|
|
||||||
} else {
|
|
||||||
messenger.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Gagal menambah produk: $err'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: const Text('Tambah Produk'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user