Kasir: indikator loading produk + empty/error state + tombol Coba lagi, refresh retry, login tanpa loading global

This commit is contained in:
Onie
2026-10-18 16:30:44 +07:00
parent aba36f5c60
commit 0174ca4f64
3 changed files with 193 additions and 29 deletions

View File

@@ -43,6 +43,8 @@ class AppState extends ChangeNotifier {
}
}
Future<void> retryProducts() => refresh();
// 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);
@@ -69,6 +71,9 @@ class AppState extends ChangeNotifier {
List<Map<String, dynamic>> kasirAccounts = [];
bool loading = true;
String? error;
bool dataReady = false;
bool productsLoading = false;
String? productsError;
static const int _pageSize = 30;
int _visibleProductCount = _pageSize;
@@ -196,7 +201,12 @@ class AppState extends ChangeNotifier {
_salesFirstLoad = false;
}
Future<void> refresh() async {
/// Refresh produk + riwayat dengan retry terbatas untuk kegagalan sementara.
/// Nyalakan indikator loading hanya saat data pertama belum siap, supaya
/// Kasir bisa menampilkan status memuat/gagal/kosong dengan jelas.
/// Nonaktifkan indikator apa pun yang menyebabkan masalah refresh berulang.
/// Kirim sinyal hanya pada pemuatan awal; polling latar tidak mengganggu.
Future<void> refresh({bool notifyLoading = true, int retries = 1}) async {
final authOk = await _ensureSessionAndAuth();
if (!authOk) {
// Auth mati: jangan lanjut query (pasti 401). Cukup notify agar
@@ -204,9 +214,34 @@ class AppState extends ChangeNotifier {
notifyListeners();
return;
}
await _loadProducts();
await _loadSales();
notifyListeners();
final showProductsLoading = notifyLoading && !dataReady;
if (showProductsLoading) {
productsLoading = true;
notifyListeners();
}
try {
await _loadProducts();
await _loadSales();
dataReady = true;
} catch (e) {
productsError ??= e.toString();
if (retries > 0) {
// Coba sekali lagi untuk kegagalan sementara (misal koneksi putus).
productsError = null;
await Future<void>.delayed(const Duration(seconds: 1));
await refresh(notifyLoading: notifyLoading, retries: retries - 1);
return;
}
debugPrint('Gagal refresh: $e');
} finally {
if (showProductsLoading) {
productsLoading = false;
}
if (notifyLoading) {
notifyListeners();
}
}
}
final OfflineQueueService _offlineQueue = OfflineQueueService();
@@ -313,17 +348,20 @@ class AppState extends ChangeNotifier {
);
userId = res.user?.id;
_sessionExpired = false;
await _loadProfile();
// Muat produk/riwayat langsung agar user tidak menunggu poll berikut.
// Gagal load tak mematikan proses login.
try {
await refresh();
} catch (_) {}
dataReady = false;
productsLoading = true;
productsError = null;
notifyListeners();
await _loadProfile();
await refresh();
return null;
} on AuthException catch (e) {
productsLoading = false;
notifyListeners();
return e.message;
} catch (e) {
productsLoading = false;
notifyListeners();
return e.toString();
}
}
@@ -357,6 +395,10 @@ class AppState extends ChangeNotifier {
_profile = null;
cart.clear();
_discount = 0;
products = [];
dataReady = false;
productsLoading = false;
productsError = null;
notifyListeners();
}