diff --git a/.gitignore b/.gitignore index 9373a56..8161b61 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,17 @@ migrate_working_dir/ lib/config.dart 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 app.*.symbols diff --git a/PRD.md b/PRD.md index c740ec9..b4aee77 100644 --- a/PRD.md +++ b/PRD.md @@ -2,6 +2,18 @@ ## 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 - 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. @@ -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. - 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. + +## 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) + + diff --git a/check_addform.py b/check_addform.py deleted file mode 100644 index 9df7224..0000000 --- a/check_addform.py +++ /dev/null @@ -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!') diff --git a/check_addform_balance.py b/check_addform_balance.py deleted file mode 100644 index 561eba1..0000000 --- a/check_addform_balance.py +++ /dev/null @@ -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!') diff --git a/check_addform_parens.py b/check_addform_parens.py deleted file mode 100644 index 10f2e8a..0000000 --- a/check_addform_parens.py +++ /dev/null @@ -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}') diff --git a/check_addform_parens2.py b/check_addform_parens2.py deleted file mode 100644 index e940615..0000000 --- a/check_addform_parens2.py +++ /dev/null @@ -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}') diff --git a/check_depth.py b/check_depth.py deleted file mode 100644 index efc8d74..0000000 --- a/check_depth.py +++ /dev/null @@ -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}') diff --git a/check_depth2.py b/check_depth2.py deleted file mode 100644 index 7a23fd0..0000000 --- a/check_depth2.py +++ /dev/null @@ -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()}') diff --git a/check_parens.py b/check_parens.py deleted file mode 100644 index 794b6e8..0000000 --- a/check_parens.py +++ /dev/null @@ -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}') diff --git a/check_parens_detail.py b/check_parens_detail.py deleted file mode 100644 index c9b42be..0000000 --- a/check_parens_detail.py +++ /dev/null @@ -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()}') diff --git a/check_parens_no_strings.py b/check_parens_no_strings.py deleted file mode 100644 index fd0aece..0000000 --- a/check_parens_no_strings.py +++ /dev/null @@ -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}') diff --git a/check_strings.py b/check_strings.py deleted file mode 100644 index c1cdf89..0000000 --- a/check_strings.py +++ /dev/null @@ -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()}') diff --git a/check_unclosed.py b/check_unclosed.py deleted file mode 100644 index 56225fa..0000000 --- a/check_unclosed.py +++ /dev/null @@ -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!') diff --git a/check_unclosed2.py b/check_unclosed2.py deleted file mode 100644 index e2130c2..0000000 --- a/check_unclosed2.py +++ /dev/null @@ -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!') diff --git a/fix_indent.py b/fix_indent.py deleted file mode 100644 index 3eab4cb..0000000 --- a/fix_indent.py +++ /dev/null @@ -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') diff --git a/git.txt b/git.txt deleted file mode 100644 index 21063b6..0000000 --- a/git.txt +++ /dev/null @@ -1 +0,0 @@ -https://github.com/oniedrive31-star/app-grosir.git \ No newline at end of file diff --git a/logo.png b/logo.png deleted file mode 100644 index c1670f4..0000000 Binary files a/logo.png and /dev/null differ diff --git a/logo2.png b/logo2.png deleted file mode 100644 index de080e2..0000000 Binary files a/logo2.png and /dev/null differ diff --git a/ss/Screenshot_1784141157.png b/ss/Screenshot_1784141157.png deleted file mode 100644 index 2eb11c7..0000000 Binary files a/ss/Screenshot_1784141157.png and /dev/null differ diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b3f02cf..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -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); - }); -} diff --git a/test_syntax.dart b/test_syntax.dart deleted file mode 100644 index c4356cb..0000000 --- a/test_syntax.dart +++ /dev/null @@ -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')), - ], - ), - ), - ), - ], - ), - ); - }, - ); -} diff --git a/test_syntax2.dart b/test_syntax2.dart deleted file mode 100644 index 1280b68..0000000 --- a/test_syntax2.dart +++ /dev/null @@ -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(), - ]; - final _scrollTimers = {}; - 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( - 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'), - ), - ), - ], - ), - ), - ], - ), - ); - }, - ); -}