From 808992f9bd18d15c91fc2b3373ba492c9e52d6cc Mon Sep 17 00:00:00 2001 From: Onie Date: Sun, 18 Oct 2026 03:06:23 +0700 Subject: [PATCH] Notifikasi transaksi: hanya untuk admin + transaksi dari orang lain (bukan transaksi sendiri) --- lib/main.dart | 40 +++++++++++++++++++++++++++++++++++++ lib/state/app_state.dart | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 0365c33..60f70fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'config.dart'; +import 'models/sale.dart'; import 'state/app_state.dart'; import 'services/printer_service.dart'; import 'screens/login_screen.dart'; import 'screens/home_page.dart'; import 'utils/colors.dart'; +import 'utils/format.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -25,12 +28,48 @@ class MyApp extends StatefulWidget { class _MyAppState extends State with WidgetsBindingObserver { late final AppState _appState; + final GlobalKey _scaffoldKey = + GlobalKey(); @override void initState() { super.initState(); _appState = AppState(); WidgetsBinding.instance.addObserver(this); + // Dengarkan stream notifikasi transaksi baru (dari HP lain / checkout). + _appState.newSaleNotifications.stream.listen(_showSaleNotification); + } + + void _showSaleNotification(Sale sale) { + final messenger = _scaffoldKey.currentState; + if (messenger == null) return; + // Getar + bunyi notifikasi. + HapticFeedback.heavyImpact(); + SystemSound.play(SystemSoundType.click); + // Banner notifikasi di atas layar. + messenger.showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: kSuccess, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Transaksi baru: ${sale.items.fold(0, (s, i) => s + i.qty)} item • ${formatRupiah(sale.total)}', + style: const TextStyle(color: kText, fontWeight: FontWeight.w600), + ), + ), + ], + ), + backgroundColor: kSurface, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: const BorderSide(color: kSuccess, width: 1), + ), + ), + ); } @override @@ -54,6 +93,7 @@ class _MyAppState extends State with WidgetsBindingObserver { return ChangeNotifierProvider.value( value: _appState, child: MaterialApp( + scaffoldMessengerKey: _scaffoldKey, title: 'Dutra Store', debugShowCheckedModeBanner: false, theme: ThemeData( diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index ccaaadf..c0f9dff 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -27,6 +27,12 @@ class CartLine { class AppState extends ChangeNotifier { final SupabaseClient _supabase = Supabase.instance.client; + // Stream notifikasi transaksi baru (dipakai UI untuk banner+suara+getar). + // Diisi setiap createSale sukses DAN setiap realtime INSERT dari HP lain. + final StreamController newSaleNotifications = + StreamController.broadcast(); + final Set _notifiedSaleIds = {}; + // Client dengan service_role key untuk operasi admin (buat/edit/hapus akun kasir). // PERINGATAN: key ini bypass RLS — hanya untuk app POS di device terpercaya. final SupabaseClient _admin = SupabaseClient(supabaseUrl, supabaseServiceKey); @@ -135,6 +141,38 @@ class AppState extends ChangeNotifier { if (elapsed < const Duration(seconds: 15)) return; } await refresh(); + _checkNewSales(); + } + + bool _salesFirstLoad = true; + + /// Deteksi transaksi baru dari polling/refresh dan kirim ke stream + /// notifikasi. Load pertama dilewati agar tidak spam saat start. + void _checkNewSales() { + // Hanya admin yang perlu notifikasi — kasir tidak (mereka yang + // transaksi, jadi sudah tahu). + if (userRole != 'admin') { + for (final s in sales) { + _notifiedSaleIds.add(s.id); + } + _salesFirstLoad = false; + return; + } + final now = DateTime.now(); + for (final s in sales) { + if (!_notifiedSaleIds.contains(s.id)) { + _notifiedSaleIds.add(s.id); + // Lewati transaksi lama (>5 menit) agar tidak spam notif saat start. + // Lewati juga transaksi sendiri — admin tahu kalau dia yang jualan. + if (!_salesFirstLoad && + s.userId != userId && + now.difference(s.date).inMinutes.abs() <= 5 && + !newSaleNotifications.isClosed) { + newSaleNotifications.add(s); + } + } + } + _salesFirstLoad = false; } Future refresh() async { @@ -686,6 +724,11 @@ class AppState extends ChangeNotifier { cart.clear(); _discount = 0; notifyListeners(); + + // Tandai ID transaksi lokal agar tidak di-notify lagi via polling + // (admin yang jualan sendiri tidak perlu notif transaksinya sendiri). + _notifiedSaleIds.add(sale.id); + return sale; } catch (e) { await _offlineQueue.addToQueue({