Add glitchtip for centralized error logging

This commit is contained in:
2026-07-30 02:32:30 +02:00
parent 498ad68e1c
commit c210f80f27
15 changed files with 416 additions and 193 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:kooltab2/views/settings_view.dart'; import 'package:kooltab2/views/settings_view.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../viewmodels/pin_lock_view_model.dart'; import '../viewmodels/pin_lock_view_model.dart';
import '../views/bar_screen_view.dart'; import '../views/bar_screen_view.dart';
@@ -16,7 +17,7 @@ final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
GoRouter createAppRouter(PinLockViewModel pinLockViewModel) { GoRouter createAppRouter(PinLockViewModel pinLockViewModel) {
return GoRouter( return GoRouter(
initialLocation: '/bar', initialLocation: '/bar',
observers: [routeObserver], observers: [routeObserver, SentryNavigatorObserver()],
refreshListenable: pinLockViewModel, refreshListenable: pinLockViewModel,
redirect: (context, state) { redirect: (context, state) {
if (!pinLockViewModel.hasLoaded) return null; if (!pinLockViewModel.hasLoaded) return null;
+69 -50
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
@@ -10,6 +11,7 @@ import 'package:kooltab2/viewmodels/inventory_view_model.dart';
import 'package:kooltab2/viewmodels/pin_lock_view_model.dart'; import 'package:kooltab2/viewmodels/pin_lock_view_model.dart';
import 'package:kooltab2/viewmodels/settings_view_model.dart'; import 'package:kooltab2/viewmodels/settings_view_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'app/app.dart'; import 'app/app.dart';
import 'app/app_bootstrap.dart'; import 'app/app_bootstrap.dart';
@@ -41,60 +43,77 @@ Future<void> main() async {
final appRouter = createAppRouter(pinLockViewModel); final appRouter = createAppRouter(pinLockViewModel);
runApp( await SentryFlutter.init(
MultiProvider( (options) {
providers: [ options.dsn =
Provider<AppDatabase>.value(value: database), 'https://2c31c1baf3ee495288ad08a56d7baf0e@glitchtip.brammie15.dev/1';
Provider<ProductService>( options.tracesSampleRate = kReleaseMode ? 0.1 : 1.0;
create: (context) => options.enableAutoSessionTracking = false;
DriftProductService(database: context.read<AppDatabase>()), options.attachScreenshot = true;
), options.attachViewHierarchy = true;
Provider<BarTabService>( options.enableLogs = true;
create: (context) => options.attachThreads = true;
DriftBarTabService(database: context.read<AppDatabase>()),
), options.replay.onErrorSampleRate = 1.0;
Provider<SettingsService>( options.replay.sessionSampleRate = 0.05;
create: (context) => },
DriftSettingsService(database: context.read<AppDatabase>()), appRunner: () => runApp(
), MultiProvider(
ChangeNotifierProvider<InventoryViewModel>( providers: [
create: (context) => Provider<AppDatabase>.value(value: database),
InventoryViewModel(productService: context.read<ProductService>()) Provider<ProductService>(
..load(), create: (context) =>
), DriftProductService(database: context.read<AppDatabase>()),
ChangeNotifierProvider<ProductListViewModel>(
create: (context) => ProductListViewModel(
productService: context.read<ProductService>(),
inventory: context.read<InventoryViewModel>(),
), ),
), Provider<BarTabService>(
ChangeNotifierProvider<BarScreenViewModel>( create: (context) =>
create: (context) => BarScreenViewModel( DriftBarTabService(database: context.read<AppDatabase>()),
barTabService: context.read<BarTabService>(),
inventory: context.read<InventoryViewModel>(),
), ),
), Provider<SettingsService>(
ChangeNotifierProvider<HistoryViewModel>( create: (context) =>
create: (context) => DriftSettingsService(database: context.read<AppDatabase>()),
HistoryViewModel(barTabService: context.read<BarTabService>()),
),
ChangeNotifierProvider<PinLockViewModel>.value(value: pinLockViewModel),
ChangeNotifierProvider<SettingsViewModel>(
create: (context) => SettingsViewModel(
settingsService: context.read<SettingsService>(),
), ),
), ChangeNotifierProvider<InventoryViewModel>(
ChangeNotifierProvider<DevMenuViewModel>( create: (context) => InventoryViewModel(
create: (context) => DevMenuViewModel( productService: context.read<ProductService>(),
database: context.read<AppDatabase>(), )..load(),
productService: context.read<ProductService>(),
barTabService: context.read<BarTabService>(),
settingsService: context.read<SettingsService>(),
pinLockService: PinLockService(),
), ),
), ChangeNotifierProvider<ProductListViewModel>(
], create: (context) => ProductListViewModel(
child: AppBootstrap(child: KoolTabApp(router: appRouter)), productService: context.read<ProductService>(),
inventory: context.read<InventoryViewModel>(),
),
),
ChangeNotifierProvider<BarScreenViewModel>(
create: (context) => BarScreenViewModel(
barTabService: context.read<BarTabService>(),
inventory: context.read<InventoryViewModel>(),
),
),
ChangeNotifierProvider<HistoryViewModel>(
create: (context) =>
HistoryViewModel(barTabService: context.read<BarTabService>()),
),
ChangeNotifierProvider<PinLockViewModel>.value(
value: pinLockViewModel,
),
ChangeNotifierProvider<SettingsViewModel>(
create: (context) => SettingsViewModel(
settingsService: context.read<SettingsService>(),
),
),
ChangeNotifierProvider<DevMenuViewModel>(
create: (context) => DevMenuViewModel(
database: context.read<AppDatabase>(),
productService: context.read<ProductService>(),
barTabService: context.read<BarTabService>(),
settingsService: context.read<SettingsService>(),
pinLockService: PinLockService(),
),
),
],
child: AppBootstrap(child: KoolTabApp(router: appRouter)),
),
), ),
); );
} }
+6 -8
View File
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kooltab2/utils/app_update_util.dart'; import 'package:kooltab2/utils/app_update_util.dart';
import 'package:ota_update/ota_update.dart'; import 'package:ota_update/ota_update.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'app_config.dart'; import 'app_config.dart';
@@ -86,34 +87,31 @@ Future<void> _downloadAndInstall(UpdateInfo update) async {
) )
.listen((OtaEvent event) { .listen((OtaEvent event) {
debugPrint("OTA status: ${event.status}"); debugPrint("OTA status: ${event.status}");
debugPrint("OTA value: ${event.value}"); debugPrint("OTA value: ${event.value}");
switch (event.status) { switch (event.status) {
case OtaStatus.DOWNLOADING: case OtaStatus.DOWNLOADING:
final progress = double.tryParse(event.value ?? "0") ?? 0; final progress = double.tryParse(event.value ?? "0") ?? 0;
debugPrint("Downloading ${progress.toStringAsFixed(0)}%"); debugPrint("Downloading ${progress.toStringAsFixed(0)}%");
break; break;
case OtaStatus.INSTALLING: case OtaStatus.INSTALLING:
debugPrint("Installing update"); debugPrint("Installing update");
break; break;
case OtaStatus.INSTALLATION_ERROR: case OtaStatus.INSTALLATION_ERROR:
debugPrint("Installation error: ${event.value}"); debugPrint("Installation error: ${event.value}");
break; break;
case OtaStatus.DOWNLOAD_ERROR: case OtaStatus.DOWNLOAD_ERROR:
debugPrint("Download error: ${event.value}"); debugPrint("Download error: ${event.value}");
break; break;
default: default:
break; break;
} }
}, onError: (e, stack) {
debugPrint("OTA stream error: $e");
Sentry.captureException(e, stackTrace: stack);
}); });
} catch (e) { } catch (e, stack) {
debugPrint("OTA update failed: $e"); debugPrint("OTA update failed: $e");
Sentry.captureException(e, stackTrace: stack);
} }
} }
+81 -35
View File
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/payment_method.dart'; import '../models/payment_method.dart';
import '../models/bar_tab.dart'; import '../models/bar_tab.dart';
@@ -76,10 +77,16 @@ class BarScreenViewModel extends ChangeNotifier {
} }
Future<void> createTab(String customerName) async { Future<void> createTab(String customerName) async {
final tab = await barTabService.createTab(customerName: customerName); try {
final tab = await barTabService.createTab(customerName: customerName);
_selectedTabId = tab.id; _selectedTabId = tab.id;
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: createTab error: $e');
_errorMessage = 'Could not create tab.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
}
} }
Future<void> addProductToSelectedTab(Product product) async { Future<void> addProductToSelectedTab(Product product) async {
@@ -88,34 +95,52 @@ class BarScreenViewModel extends ChangeNotifier {
if (tab == null) return; if (tab == null) return;
if (product.stockQuantity <= 0) { if (product.stockQuantity <= 0) {
throw Exception('Product is out of stock.'); _errorMessage = 'Product is out of stock.';
notifyListeners();
return;
} }
await barTabService.addProductToTab( try {
tabId: tab.id, await barTabService.addProductToTab(
product: product, tabId: tab.id,
stockAdjustment: () => inventory.decreaseStock(product.id, 1), product: product,
); stockAdjustment: () => inventory.decreaseStock(product.id, 1),
);
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: addProductToSelectedTab error: $e');
_errorMessage = 'Could not add product to tab.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> changeItemQuantity(TabItem item, int quantity) async { Future<void> changeItemQuantity(TabItem item, int quantity) async {
final difference = quantity - item.quantity; final difference = quantity - item.quantity;
await barTabService.updateTabItemQuantity( try {
tabItemId: item.id, await barTabService.updateTabItemQuantity(
quantity: quantity, tabItemId: item.id,
stockAdjustment: () async { quantity: quantity,
if (difference > 0) { stockAdjustment: () async {
await inventory.decreaseStock(item.productId, difference); if (difference > 0) {
} else if (difference < 0) { await inventory.decreaseStock(item.productId, difference);
await inventory.increaseStock(item.productId, -difference); } else if (difference < 0) {
} await inventory.increaseStock(item.productId, -difference);
}, }
); },
);
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: changeItemQuantity error: $e');
_errorMessage = 'Could not update item quantity.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> closeSelectedTab({PaymentMethod paymentMethod = PaymentMethod.cash}) async { Future<void> closeSelectedTab({PaymentMethod paymentMethod = PaymentMethod.cash}) async {
@@ -123,25 +148,46 @@ class BarScreenViewModel extends ChangeNotifier {
if (tab == null) return; if (tab == null) return;
await barTabService.closeTab(tab.id, paymentMethod: paymentMethod); try {
await barTabService.closeTab(tab.id, paymentMethod: paymentMethod);
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: closeSelectedTab error: $e');
_errorMessage = 'Could not close tab.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> closeTab(String tabId, {PaymentMethod paymentMethod = PaymentMethod.cash}) async { Future<void> closeTab(String tabId, {PaymentMethod paymentMethod = PaymentMethod.cash}) async {
await barTabService.closeTab(tabId, paymentMethod: paymentMethod); try {
await barTabService.closeTab(tabId, paymentMethod: paymentMethod);
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: closeTab error: $e');
_errorMessage = 'Could not close tab.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> _reloadTabs() async { Future<void> _reloadTabs() async {
_tabs = await barTabService.getOpenTabs(); try {
_tabs = await barTabService.getOpenTabs();
if (_selectedTabId == null || if (_selectedTabId == null ||
!_tabs.any((tab) => tab.id == _selectedTabId)) { !_tabs.any((tab) => tab.id == _selectedTabId)) {
_selectedTabId = _tabs.isEmpty ? null : _tabs.first.id; _selectedTabId = _tabs.isEmpty ? null : _tabs.first.id;
}
notifyListeners();
} catch (e, stack) {
debugPrint('BarScreenViewModel: reloadTabs error: $e');
_errorMessage = 'Could not reload tabs.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
} }
notifyListeners();
} }
} }
+7
View File
@@ -6,6 +6,7 @@ import 'package:kooltab2/models/payment_method.dart';
import 'package:kooltab2/services/pin_lock_service.dart'; import 'package:kooltab2/services/pin_lock_service.dart';
import 'package:kooltab2/services/product_service.dart'; import 'package:kooltab2/services/product_service.dart';
import 'package:kooltab2/services/settings_service.dart'; import 'package:kooltab2/services/settings_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class DevMenuViewModel extends ChangeNotifier { class DevMenuViewModel extends ChangeNotifier {
@@ -255,6 +256,12 @@ class DevMenuViewModel extends ChangeNotifier {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
try {
throw Exception('Test GlitchTip error!');
} catch (exception, stackTrace) {
Sentry.captureException(exception, stackTrace: stackTrace);
}
} }
Future<void> generateMockOrders({int count = 100}) async { Future<void> generateMockOrders({int count = 100}) async {
+37 -18
View File
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/product.dart'; import '../models/product.dart';
import '../services/product_service.dart'; import '../services/product_service.dart';
@@ -13,35 +14,53 @@ class InventoryViewModel extends ChangeNotifier {
List<Product> get products => _products; List<Product> get products => _products;
Future<void> load() async { Future<void> load() async {
_products = await productService.getProducts(); try {
notifyListeners(); _products = await productService.getProducts();
notifyListeners();
} catch (e, stack) {
debugPrint('InventoryViewModel: load error: $e');
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> decreaseStock(String productId, int amount) async { Future<void> decreaseStock(String productId, int amount) async {
await productService.decreaseStock(productId, amount); try {
await productService.decreaseStock(productId, amount);
final index = _products.indexWhere((product) => product.id == productId); final index = _products.indexWhere((product) => product.id == productId);
if (index != -1) { if (index != -1) {
_products[index] = _products[index].copyWith( _products[index] = _products[index].copyWith(
stockQuantity: _products[index].stockQuantity - amount, stockQuantity: _products[index].stockQuantity - amount,
); );
}
notifyListeners();
} catch (e, stack) {
debugPrint('InventoryViewModel: decreaseStock error: $e');
Sentry.captureException(e, stackTrace: stack);
rethrow;
} }
notifyListeners();
} }
Future<void> increaseStock(String productId, int amount) async { Future<void> increaseStock(String productId, int amount) async {
await productService.increaseStock(productId, amount); try {
await productService.increaseStock(productId, amount);
final index = _products.indexWhere((product) => product.id == productId); final index = _products.indexWhere((product) => product.id == productId);
if (index != -1) { if (index != -1) {
_products[index] = _products[index].copyWith( _products[index] = _products[index].copyWith(
stockQuantity: _products[index].stockQuantity + amount, stockQuantity: _products[index].stockQuantity + amount,
); );
}
notifyListeners();
} catch (e, stack) {
debugPrint('InventoryViewModel: increaseStock error: $e');
Sentry.captureException(e, stackTrace: stack);
rethrow;
} }
notifyListeners();
} }
} }
+38 -13
View File
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/product.dart'; import '../models/product.dart';
import '../services/product_service.dart'; import '../services/product_service.dart';
@@ -66,26 +67,50 @@ class ProductListViewModel extends ChangeNotifier {
required int priceInCents, required int priceInCents,
required String? imagePath, required String? imagePath,
}) async { }) async {
await productService.createProduct( try {
name: name, await productService.createProduct(
category: category, name: name,
stockQuantity: stockQuantity, category: category,
lowStockThreshold: lowStockThreshold, stockQuantity: stockQuantity,
priceInCents: priceInCents, lowStockThreshold: lowStockThreshold,
imagePath: imagePath, priceInCents: priceInCents,
); imagePath: imagePath,
);
await loadProducts(); await loadProducts();
} catch (e, stack) {
debugPrint('ProductListViewModel: addProduct error: $e');
_errorMessage = 'Could not add product.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> updateProduct(Product product) async { Future<void> updateProduct(Product product) async {
await productService.updateProduct(product); try {
await loadProducts(); await productService.updateProduct(product);
await loadProducts();
} catch (e, stack) {
debugPrint('ProductListViewModel: updateProduct error: $e');
_errorMessage = 'Could not update product.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<void> deleteProduct(String id) async { Future<void> deleteProduct(String id) async {
await productService.deleteProduct(id); try {
await loadProducts(); await productService.deleteProduct(id);
await loadProducts();
} catch (e, stack) {
debugPrint('ProductListViewModel: deleteProduct error: $e');
_errorMessage = 'Could not delete product.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
} }
Future<Product?> getProductById(String id) { Future<Product?> getProductById(String id) {
+47 -11
View File
@@ -6,6 +6,7 @@ import 'package:kooltab2/views/dialogs/close_tab_dialog.dart';
import 'package:kooltab2/views/dialogs/new_tab_dialog.dart'; import 'package:kooltab2/views/dialogs/new_tab_dialog.dart';
import 'package:kooltab2/views/widgets/product_tile.dart'; import 'package:kooltab2/views/widgets/product_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import '../models/bar_tab.dart'; import '../models/bar_tab.dart';
@@ -53,7 +54,7 @@ class _BarScreenViewState extends State<BarScreenView> {
const SizedBox(width: 6), const SizedBox(width: 6),
IconButton( IconButton(
tooltip: 'Refresh', tooltip: 'Refresh',
onPressed: viewModel.load, onPressed: () => viewModel.load(),
icon: const Icon(Icons.refresh_rounded), icon: const Icon(Icons.refresh_rounded),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
@@ -127,7 +128,8 @@ class _BarScreenViewState extends State<BarScreenView> {
try { try {
await viewModel.addProductToSelectedTab(product); await viewModel.addProductToSelectedTab(product);
} catch (e) { } catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$e')), SnackBar(content: Text('$e')),
@@ -304,7 +306,7 @@ class _TabPanel extends StatefulWidget {
final ValueChanged<String> onTabSelected; final ValueChanged<String> onTabSelected;
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged; final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
final VoidCallback onCloseTabPressed; final VoidCallback onCloseTabPressed;
final ValueChanged<String> onTabClosed; final Future<void> Function(String) onTabClosed;
const _TabPanel({ const _TabPanel({
required this.tabs, required this.tabs,
@@ -460,7 +462,7 @@ class _OpenTabsList extends StatelessWidget {
final List<BarTab> tabs; final List<BarTab> tabs;
final String? selectedTabId; final String? selectedTabId;
final ValueChanged<String> onTabSelected; final ValueChanged<String> onTabSelected;
final ValueChanged<String> onTabClosed; final Future<void> Function(String) onTabClosed;
const _OpenTabsList({ const _OpenTabsList({
required this.tabs, required this.tabs,
@@ -498,17 +500,25 @@ class _OpenTabsList extends StatelessWidget {
extentRatio: 0.6, extentRatio: 0.6,
children: [ children: [
SlidableAction( SlidableAction(
onPressed: (_) { onPressed: (_) => onTabSelected(tab.id),
onTabSelected(tab.id);
// TODO: rename/edit tab
},
icon: Icons.edit_outlined, icon: Icons.edit_outlined,
label: 'Edit', label: 'Edit',
backgroundColor: Theme.of(context).colorScheme.secondary, backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary, foregroundColor: Theme.of(context).colorScheme.onSecondary,
), ),
SlidableAction( SlidableAction(
onPressed: (_) => onTabClosed(tab.id), onPressed: (_) async {
try {
await onTabClosed(tab.id);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not close tab: $e')),
);
}
}
},
icon: Icons.close_rounded, icon: Icons.close_rounded,
label: 'Close', label: 'Close',
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
@@ -723,7 +733,20 @@ class _TabItemRow extends StatelessWidget {
children: [ children: [
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () => onQuantityChanged(item, item.quantity - 1), onPressed: () async {
try {
await onQuantityChanged(item, item.quantity - 1);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not update quantity: $e'),
),
);
}
}
},
icon: const Icon(Icons.remove_rounded, size: 18), icon: const Icon(Icons.remove_rounded, size: 18),
), ),
SizedBox( SizedBox(
@@ -736,7 +759,20 @@ class _TabItemRow extends StatelessWidget {
), ),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () => onQuantityChanged(item, item.quantity + 1), onPressed: () async {
try {
await onQuantityChanged(item, item.quantity + 1);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not update quantity: $e'),
),
);
}
}
},
icon: const Icon(Icons.add_rounded, size: 18), icon: const Icon(Icons.add_rounded, size: 18),
), ),
], ],
+10 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../models/payment_method.dart'; import '../../models/payment_method.dart';
import '../../viewmodels/bar_screen_view_model.dart'; import '../../viewmodels/bar_screen_view_model.dart';
@@ -63,7 +64,15 @@ Future<void> confirmCloseTab(BuildContext context) async {
if (confirmed != true) return; if (confirmed != true) return;
await viewModel.closeSelectedTab(paymentMethod: paymentMethod); try {
await viewModel.closeSelectedTab(paymentMethod: paymentMethod);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not close tab: $e')),
);
}
} }
class _PaymentPicker extends StatelessWidget { class _PaymentPicker extends StatelessWidget {
+10 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kooltab2/viewmodels/bar_screen_view_model.dart'; import 'package:kooltab2/viewmodels/bar_screen_view_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> showNewTabDialog(BuildContext context) async { Future<void> showNewTabDialog(BuildContext context) async {
final controller = TextEditingController(); final controller = TextEditingController();
@@ -39,5 +40,13 @@ Future<void> showNewTabDialog(BuildContext context) async {
if (!context.mounted) return; if (!context.mounted) return;
await context.read<BarScreenViewModel>().createTab(name.trim()); try {
await context.read<BarScreenViewModel>().createTab(name.trim());
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not create tab: $e')),
);
}
} }
+69 -39
View File
@@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/product.dart'; import '../models/product.dart';
import '../viewmodels/product_list_view_model.dart'; import '../viewmodels/product_list_view_model.dart';
@@ -52,30 +53,39 @@ class _ProductFormViewState extends State<ProductFormView> {
return; return;
} }
final viewModel = context.read<ProductListViewModel>(); try {
final product = await viewModel.getProductById(widget.productId!); final viewModel = context.read<ProductListViewModel>();
final product = await viewModel.getProductById(widget.productId!);
if (!mounted) return; if (!mounted) return;
if (product == null) { if (product == null) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Product not found.')));
context.go('/products');
return;
}
_existingProduct = product;
_nameController.text = product.name;
_selectedCategory = product.category;
_priceController.text = (product.priceInCents / 100).toStringAsFixed(2);
_stockController.text = product.stockQuantity.toString();
_lowStockController.text = product.lowStockThreshold.toString();
_imagePath = product.imagePath;
_hasImage = product.imagePath != null && product.imagePath!.isNotEmpty;
setState(() => _isLoading = false);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('Product not found.'))); ).showSnackBar(SnackBar(content: Text('Could not load product: $e')));
context.go('/products'); context.go('/products');
return;
} }
_existingProduct = product;
_nameController.text = product.name;
_selectedCategory = product.category;
_priceController.text = (product.priceInCents / 100).toStringAsFixed(2);
_stockController.text = product.stockQuantity.toString();
_lowStockController.text = product.lowStockThreshold.toString();
_imagePath = product.imagePath;
_hasImage = product.imagePath != null && product.imagePath!.isNotEmpty;
setState(() => _isLoading = false);
} }
@override @override
@@ -159,29 +169,39 @@ class _ProductFormViewState extends State<ProductFormView> {
final name = _nameController.text.trim(); final name = _nameController.text.trim();
final category = _selectedCategory!; final category = _selectedCategory!;
final priceInCents = _parsePriceToCents(_priceController.text); final priceInCents = _parsePriceToCents(_priceController.text);
final stockQuantity = int.parse(_stockController.text); final stockQuantity = int.tryParse(_stockController.text) ?? 0;
final lowStockThreshold = int.parse(_lowStockController.text); final lowStockThreshold = int.tryParse(_lowStockController.text) ?? 0;
if (widget.isEditing) { try {
final updatedProduct = _existingProduct!.copyWith( if (widget.isEditing) {
name: name, final updatedProduct = _existingProduct!.copyWith(
category: category, name: name,
priceInCents: priceInCents, category: category,
stockQuantity: stockQuantity, priceInCents: priceInCents,
lowStockThreshold: lowStockThreshold, stockQuantity: stockQuantity,
imagePath: _imagePath, lowStockThreshold: lowStockThreshold,
); imagePath: _imagePath,
);
await viewModel.updateProduct(updatedProduct); await viewModel.updateProduct(updatedProduct);
} else { } else {
await viewModel.addProduct( await viewModel.addProduct(
name: name, name: name,
category: category, category: category,
priceInCents: priceInCents, priceInCents: priceInCents,
stockQuantity: stockQuantity, stockQuantity: stockQuantity,
lowStockThreshold: lowStockThreshold, lowStockThreshold: lowStockThreshold,
imagePath: _imagePath, imagePath: _imagePath,
);
}
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
setState(() => _isSaving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not save product: $e')),
); );
return;
} }
if (!mounted) return; if (!mounted) return;
@@ -218,7 +238,17 @@ class _ProductFormViewState extends State<ProductFormView> {
if (confirmed != true) return; if (confirmed != true) return;
final viewModel = context.read<ProductListViewModel>(); final viewModel = context.read<ProductListViewModel>();
await viewModel.deleteProduct(widget.productId!);
try {
await viewModel.deleteProduct(widget.productId!);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not delete product: $e')),
);
return;
}
if (!mounted) return; if (!mounted) return;
+19 -4
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/settings.dart'; import '../models/settings.dart';
import '../utils/app_update_util.dart'; import '../utils/app_update_util.dart';
@@ -117,7 +118,13 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
} }
pinLockViewModel.setPinRequired(true); pinLockViewModel.setPinRequired(true);
await context.read<SettingsViewModel>().updatePinRequired(true);
try {
await context.read<SettingsViewModel>().updatePinRequired(true);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
_showError('Could not save PIN setting.');
}
} }
Future<void> _changePin() async { Future<void> _changePin() async {
@@ -156,7 +163,13 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
} }
pinLockViewModel.setPinRequired(false); pinLockViewModel.setPinRequired(false);
await context.read<SettingsViewModel>().updatePinRequired(false);
try {
await context.read<SettingsViewModel>().updatePinRequired(false);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
_showError('Could not save PIN setting.');
}
} }
@override @override
@@ -317,7 +330,8 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
} }
_showUpdateDialog(update); _showUpdateDialog(update);
} catch (e) { } catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( ScaffoldMessenger.of(
@@ -360,7 +374,8 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
); );
}, },
); );
} catch (e) { } catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( ScaffoldMessenger.of(
BIN
View File
Binary file not shown.
+20 -12
View File
@@ -516,18 +516,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: jni name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "0.14.2"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -708,10 +700,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.2.23"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -816,6 +808,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
sentry:
dependency: transitive
description:
name: sentry
sha256: a84bf3a83b3ce1c89fce28b9f97659e3826df14a73a256952465aac2c5efdf5a
url: "https://pub.dev"
source: hosted
version: "9.25.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: c85575266d91f57364e9b4cb522835156ec3c5581c78339dc7fae491cd0c6f76
url: "https://pub.dev"
source: hosted
version: "9.25.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
+1
View File
@@ -51,6 +51,7 @@ dependencies:
open_filex: ^4.7.0 open_filex: ^4.7.0
ota_update: ^7.1.0 ota_update: ^7.1.0
flutter_svg: ^2.3.0 flutter_svg: ^2.3.0
sentry_flutter: ^9.25.0
# permission_handler: ^12.0.3 # permission_handler: ^12.0.3