Notifikasi transaksi: hanya untuk admin + transaksi dari orang lain (bukan transaksi sendiri)
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'config.dart';
|
import 'config.dart';
|
||||||
|
import 'models/sale.dart';
|
||||||
import 'state/app_state.dart';
|
import 'state/app_state.dart';
|
||||||
import 'services/printer_service.dart';
|
import 'services/printer_service.dart';
|
||||||
import 'screens/login_screen.dart';
|
import 'screens/login_screen.dart';
|
||||||
import 'screens/home_page.dart';
|
import 'screens/home_page.dart';
|
||||||
import 'utils/colors.dart';
|
import 'utils/colors.dart';
|
||||||
|
import 'utils/format.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -25,12 +28,48 @@ class MyApp extends StatefulWidget {
|
|||||||
|
|
||||||
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
late final AppState _appState;
|
late final AppState _appState;
|
||||||
|
final GlobalKey<ScaffoldMessengerState> _scaffoldKey =
|
||||||
|
GlobalKey<ScaffoldMessengerState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_appState = AppState();
|
_appState = AppState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
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<int>(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
|
@override
|
||||||
@@ -54,6 +93,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
return ChangeNotifierProvider.value(
|
return ChangeNotifierProvider.value(
|
||||||
value: _appState,
|
value: _appState,
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
scaffoldMessengerKey: _scaffoldKey,
|
||||||
title: 'Dutra Store',
|
title: 'Dutra Store',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ class CartLine {
|
|||||||
class AppState extends ChangeNotifier {
|
class AppState extends ChangeNotifier {
|
||||||
final SupabaseClient _supabase = Supabase.instance.client;
|
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<Sale> newSaleNotifications =
|
||||||
|
StreamController<Sale>.broadcast();
|
||||||
|
final Set<String> _notifiedSaleIds = {};
|
||||||
|
|
||||||
// Client dengan service_role key untuk operasi admin (buat/edit/hapus akun kasir).
|
// 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.
|
// PERINGATAN: key ini bypass RLS — hanya untuk app POS di device terpercaya.
|
||||||
final SupabaseClient _admin = SupabaseClient(supabaseUrl, supabaseServiceKey);
|
final SupabaseClient _admin = SupabaseClient(supabaseUrl, supabaseServiceKey);
|
||||||
@@ -135,6 +141,38 @@ class AppState extends ChangeNotifier {
|
|||||||
if (elapsed < const Duration(seconds: 15)) return;
|
if (elapsed < const Duration(seconds: 15)) return;
|
||||||
}
|
}
|
||||||
await refresh();
|
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<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
@@ -686,6 +724,11 @@ class AppState extends ChangeNotifier {
|
|||||||
cart.clear();
|
cart.clear();
|
||||||
_discount = 0;
|
_discount = 0;
|
||||||
notifyListeners();
|
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;
|
return sale;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await _offlineQueue.addToQueue({
|
await _offlineQueue.addToQueue({
|
||||||
|
|||||||
Reference in New Issue
Block a user