feat: add rows count (broken), fix: bug with low stock items

This commit is contained in:
2026-08-02 23:23:28 +02:00
parent f5b2041612
commit 6060ca2713
19 changed files with 373 additions and 41 deletions
+4
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import '../database/app_database.dart';
import '../services/default_product_seeder.dart';
@@ -31,6 +32,9 @@ class _AppBootstrapState extends State<AppBootstrap> {
context.read<BarScreenViewModel>().ensureLoaded();
DefaultExportService(database: database).uploadToServer();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
});
}
+19 -3
View File
@@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
part 'app_database.g.dart';
@DataClassName('ProductRow')
@@ -111,6 +110,8 @@ class AppSettingsTable extends Table {
TextColumn get language => text().withDefault(const Constant('system'))();
IntColumn get barGridRows => integer().withDefault(const Constant(3))();
@override
Set<Column> get primaryKey => {id};
}
@@ -131,7 +132,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 7;
int get schemaVersion => 8;
@override
MigrationStrategy get migration {
@@ -140,6 +141,14 @@ class AppDatabase extends _$AppDatabase {
await migrator.createAll();
},
onUpgrade: (migrator, from, to) async {
Future<bool> hasSettingsColumn(String columnName) async {
final columns = await migrator.database
.customSelect('PRAGMA table_info(app_settings)')
.get();
return columns.any((column) => column.data['name'] == columnName);
}
if (from < 2) {
await migrator.createTable(barTabs);
await migrator.createTable(tabItems);
@@ -162,9 +171,16 @@ class AppDatabase extends _$AppDatabase {
await migrator.addColumn(closedTabs, closedTabs.paymentMethod);
}
if (from < 7) {
if (from < 7 && !await hasSettingsColumn('language')) {
await migrator.addColumn(appSettingsTable, appSettingsTable.language);
}
if (from < 8 && !await hasSettingsColumn('bar_grid_rows')) {
await migrator.addColumn(
appSettingsTable,
appSettingsTable.barGridRows,
);
}
},
);
}
+81 -4
View File
@@ -2231,8 +2231,26 @@ class $AppSettingsTableTable extends AppSettingsTable
requiredDuringInsert: false,
defaultValue: const Constant('system'),
);
static const VerificationMeta _barGridRowsMeta = const VerificationMeta(
'barGridRows',
);
@override
List<GeneratedColumn> get $columns => [id, pinRequired, themeMode, language];
late final GeneratedColumn<int> barGridRows = GeneratedColumn<int>(
'bar_grid_rows',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(3),
);
@override
List<GeneratedColumn> get $columns => [
id,
pinRequired,
themeMode,
language,
barGridRows,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -2271,6 +2289,15 @@ class $AppSettingsTableTable extends AppSettingsTable
language.isAcceptableOrUnknown(data['language']!, _languageMeta),
);
}
if (data.containsKey('bar_grid_rows')) {
context.handle(
_barGridRowsMeta,
barGridRows.isAcceptableOrUnknown(
data['bar_grid_rows']!,
_barGridRowsMeta,
),
);
}
return context;
}
@@ -2296,6 +2323,10 @@ class $AppSettingsTableTable extends AppSettingsTable
DriftSqlType.string,
data['${effectivePrefix}language'],
)!,
barGridRows: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}bar_grid_rows'],
)!,
);
}
@@ -2310,11 +2341,13 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
final bool pinRequired;
final String themeMode;
final String language;
final int barGridRows;
const AppSettingsRow({
required this.id,
required this.pinRequired,
required this.themeMode,
required this.language,
required this.barGridRows,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -2323,6 +2356,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
map['pin_required'] = Variable<bool>(pinRequired);
map['theme_mode'] = Variable<String>(themeMode);
map['language'] = Variable<String>(language);
map['bar_grid_rows'] = Variable<int>(barGridRows);
return map;
}
@@ -2332,6 +2366,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
pinRequired: Value(pinRequired),
themeMode: Value(themeMode),
language: Value(language),
barGridRows: Value(barGridRows),
);
}
@@ -2345,6 +2380,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
pinRequired: serializer.fromJson<bool>(json['pinRequired']),
themeMode: serializer.fromJson<String>(json['themeMode']),
language: serializer.fromJson<String>(json['language']),
barGridRows: serializer.fromJson<int>(json['barGridRows']),
);
}
@override
@@ -2355,6 +2391,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
'pinRequired': serializer.toJson<bool>(pinRequired),
'themeMode': serializer.toJson<String>(themeMode),
'language': serializer.toJson<String>(language),
'barGridRows': serializer.toJson<int>(barGridRows),
};
}
@@ -2363,11 +2400,13 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
bool? pinRequired,
String? themeMode,
String? language,
int? barGridRows,
}) => AppSettingsRow(
id: id ?? this.id,
pinRequired: pinRequired ?? this.pinRequired,
themeMode: themeMode ?? this.themeMode,
language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
);
AppSettingsRow copyWithCompanion(AppSettingsTableCompanion data) {
return AppSettingsRow(
@@ -2377,6 +2416,9 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
: this.pinRequired,
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
language: data.language.present ? data.language.value : this.language,
barGridRows: data.barGridRows.present
? data.barGridRows.value
: this.barGridRows,
);
}
@@ -2386,13 +2428,15 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
..write('id: $id, ')
..write('pinRequired: $pinRequired, ')
..write('themeMode: $themeMode, ')
..write('language: $language')
..write('language: $language, ')
..write('barGridRows: $barGridRows')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, pinRequired, themeMode, language);
int get hashCode =>
Object.hash(id, pinRequired, themeMode, language, barGridRows);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -2400,7 +2444,8 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
other.id == this.id &&
other.pinRequired == this.pinRequired &&
other.themeMode == this.themeMode &&
other.language == this.language);
other.language == this.language &&
other.barGridRows == this.barGridRows);
}
class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
@@ -2408,12 +2453,14 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
final Value<bool> pinRequired;
final Value<String> themeMode;
final Value<String> language;
final Value<int> barGridRows;
final Value<int> rowid;
const AppSettingsTableCompanion({
this.id = const Value.absent(),
this.pinRequired = const Value.absent(),
this.themeMode = const Value.absent(),
this.language = const Value.absent(),
this.barGridRows = const Value.absent(),
this.rowid = const Value.absent(),
});
AppSettingsTableCompanion.insert({
@@ -2421,6 +2468,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
this.pinRequired = const Value.absent(),
this.themeMode = const Value.absent(),
this.language = const Value.absent(),
this.barGridRows = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id);
static Insertable<AppSettingsRow> custom({
@@ -2428,6 +2476,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
Expression<bool>? pinRequired,
Expression<String>? themeMode,
Expression<String>? language,
Expression<int>? barGridRows,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
@@ -2435,6 +2484,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
if (pinRequired != null) 'pin_required': pinRequired,
if (themeMode != null) 'theme_mode': themeMode,
if (language != null) 'language': language,
if (barGridRows != null) 'bar_grid_rows': barGridRows,
if (rowid != null) 'rowid': rowid,
});
}
@@ -2444,6 +2494,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
Value<bool>? pinRequired,
Value<String>? themeMode,
Value<String>? language,
Value<int>? barGridRows,
Value<int>? rowid,
}) {
return AppSettingsTableCompanion(
@@ -2451,6 +2502,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
pinRequired: pinRequired ?? this.pinRequired,
themeMode: themeMode ?? this.themeMode,
language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
rowid: rowid ?? this.rowid,
);
}
@@ -2470,6 +2522,9 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
if (language.present) {
map['language'] = Variable<String>(language.value);
}
if (barGridRows.present) {
map['bar_grid_rows'] = Variable<int>(barGridRows.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -2483,6 +2538,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
..write('pinRequired: $pinRequired, ')
..write('themeMode: $themeMode, ')
..write('language: $language, ')
..write('barGridRows: $barGridRows, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@@ -4071,6 +4127,7 @@ typedef $$AppSettingsTableTableCreateCompanionBuilder =
Value<bool> pinRequired,
Value<String> themeMode,
Value<String> language,
Value<int> barGridRows,
Value<int> rowid,
});
typedef $$AppSettingsTableTableUpdateCompanionBuilder =
@@ -4079,6 +4136,7 @@ typedef $$AppSettingsTableTableUpdateCompanionBuilder =
Value<bool> pinRequired,
Value<String> themeMode,
Value<String> language,
Value<int> barGridRows,
Value<int> rowid,
});
@@ -4110,6 +4168,11 @@ class $$AppSettingsTableTableFilterComposer
column: $table.language,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get barGridRows => $composableBuilder(
column: $table.barGridRows,
builder: (column) => ColumnFilters(column),
);
}
class $$AppSettingsTableTableOrderingComposer
@@ -4140,6 +4203,11 @@ class $$AppSettingsTableTableOrderingComposer
column: $table.language,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get barGridRows => $composableBuilder(
column: $table.barGridRows,
builder: (column) => ColumnOrderings(column),
);
}
class $$AppSettingsTableTableAnnotationComposer
@@ -4164,6 +4232,11 @@ class $$AppSettingsTableTableAnnotationComposer
GeneratedColumn<String> get language =>
$composableBuilder(column: $table.language, builder: (column) => column);
GeneratedColumn<int> get barGridRows => $composableBuilder(
column: $table.barGridRows,
builder: (column) => column,
);
}
class $$AppSettingsTableTableTableManager
@@ -4207,12 +4280,14 @@ class $$AppSettingsTableTableTableManager
Value<bool> pinRequired = const Value.absent(),
Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(),
Value<int> barGridRows = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => AppSettingsTableCompanion(
id: id,
pinRequired: pinRequired,
themeMode: themeMode,
language: language,
barGridRows: barGridRows,
rowid: rowid,
),
createCompanionCallback:
@@ -4221,12 +4296,14 @@ class $$AppSettingsTableTableTableManager
Value<bool> pinRequired = const Value.absent(),
Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(),
Value<int> barGridRows = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => AppSettingsTableCompanion.insert(
id: id,
pinRequired: pinRequired,
themeMode: themeMode,
language: language,
barGridRows: barGridRows,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
+3
View File
@@ -31,6 +31,9 @@
"languageSystem": "System default",
"languageEnglish": "English",
"languageDutch": "Dutch",
"productGridRows": "Product grid rows",
"rowsCount": "{count} rows",
"@rowsCount": {"placeholders": {"count": {"type": "int"}}},
"theme": "Theme",
"system": "System",
"light": "Light",
+12
View File
@@ -278,6 +278,18 @@ abstract class AppLocalizations {
/// **'Dutch'**
String get languageDutch;
/// No description provided for @productGridRows.
///
/// In en, this message translates to:
/// **'Product grid rows'**
String get productGridRows;
/// No description provided for @rowsCount.
///
/// In en, this message translates to:
/// **'{count} rows'**
String rowsCount(int count);
/// No description provided for @theme.
///
/// In en, this message translates to:
+8
View File
@@ -100,6 +100,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get languageDutch => 'Dutch';
@override
String get productGridRows => 'Product grid rows';
@override
String rowsCount(int count) {
return '$count rows';
}
@override
String get theme => 'Theme';
+8
View File
@@ -100,6 +100,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get languageDutch => 'Nederlands';
@override
String get productGridRows => 'Rijen in productraster';
@override
String rowsCount(int count) {
return '$count rijen';
}
@override
String get theme => 'Thema';
+3
View File
@@ -31,6 +31,9 @@
"languageSystem": "Systeemstandaard",
"languageEnglish": "Engels",
"languageDutch": "Nederlands",
"productGridRows": "Rijen in productraster",
"rowsCount": "{count} rijen",
"@rowsCount": {"placeholders": {"count": {"type": "int"}}},
"theme": "Thema",
"system": "Systeem",
"light": "Licht",
+9 -2
View File
@@ -1,26 +1,31 @@
enum AppThemeMode { system, light, dark, ugly }
enum AppLanguage { system, english, dutch }
class AppSettings {
final bool pinRequired;
final AppThemeMode themeMode;
final AppLanguage language;
final int barGridRows;
const AppSettings({
required this.pinRequired,
required this.themeMode,
this.language = AppLanguage.system,
this.barGridRows = 3,
});
AppSettings copyWith({
bool? pinRequired,
AppThemeMode? themeMode,
AppLanguage? language,
int? barGridRows,
}) {
return AppSettings(
pinRequired: pinRequired ?? this.pinRequired,
themeMode: themeMode ?? this.themeMode,
language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
);
}
@@ -35,8 +40,10 @@ class AppSettings {
other is AppSettings &&
pinRequired == other.pinRequired &&
themeMode == other.themeMode &&
language == other.language;
language == other.language &&
barGridRows == other.barGridRows;
@override
int get hashCode => Object.hash(pinRequired, themeMode, language);
int get hashCode =>
Object.hash(pinRequired, themeMode, language, barGridRows);
}
+8 -1
View File
@@ -4,6 +4,13 @@ import 'package:uuid/uuid.dart';
import '../database/app_database.dart';
import '../models/product.dart';
class InsufficientStockException implements Exception {
const InsufficientStockException();
@override
String toString() => 'Not enough stock';
}
abstract class ProductService {
Future<List<Product>> getProducts();
@@ -130,7 +137,7 @@ class DriftProductService implements ProductService {
}
if (product.stockQuantity < amount) {
throw Exception('Not enough stock');
throw const InsufficientStockException();
}
await updateProduct(
+2
View File
@@ -26,6 +26,7 @@ class DriftSettingsService implements SettingsService {
(language) => language.name == row.language,
orElse: () => AppLanguage.system,
),
barGridRows: row.barGridRows.clamp(2, 6).toInt(),
);
}
@@ -51,6 +52,7 @@ class DriftSettingsService implements SettingsService {
pinRequired: Value(settings.pinRequired),
themeMode: Value(settings.themeMode.name),
language: Value(settings.language.name),
barGridRows: Value(settings.barGridRows),
),
);
}
+20 -3
View File
@@ -6,6 +6,7 @@ import '../models/bar_tab.dart';
import '../models/product.dart';
import '../models/tab_item.dart';
import '../services/bar_tab_service.dart';
import '../services/product_service.dart';
import 'inventory_view_model.dart';
class BarScreenViewModel extends ChangeNotifier {
@@ -95,9 +96,7 @@ class BarScreenViewModel extends ChangeNotifier {
if (tab == null) return;
if (product.stockQuantity <= 0) {
_errorMessage = 'Product is out of stock.';
notifyListeners();
return;
throw const InsufficientStockException();
}
try {
@@ -109,6 +108,8 @@ class BarScreenViewModel extends ChangeNotifier {
await _reloadTabs();
} catch (e, stack) {
if (e is InsufficientStockException) rethrow;
debugPrint('BarScreenViewModel: addProductToSelectedTab error: $e');
_errorMessage = 'Could not add product to tab.';
notifyListeners();
@@ -120,6 +121,20 @@ class BarScreenViewModel extends ChangeNotifier {
Future<void> changeItemQuantity(TabItem item, int quantity) async {
final difference = quantity - item.quantity;
if (difference > 0) {
Product? product;
for (final candidate in inventory.products) {
if (candidate.id == item.productId) {
product = candidate;
break;
}
}
if (product != null && product.stockQuantity < difference) {
throw const InsufficientStockException();
}
}
try {
await barTabService.updateTabItemQuantity(
tabItemId: item.id,
@@ -135,6 +150,8 @@ class BarScreenViewModel extends ChangeNotifier {
await _reloadTabs();
} catch (e, stack) {
if (e is InsufficientStockException) rethrow;
debugPrint('BarScreenViewModel: changeItemQuantity error: $e');
_errorMessage = 'Could not update item quantity.';
notifyListeners();
+2
View File
@@ -38,6 +38,8 @@ class InventoryViewModel extends ChangeNotifier {
notifyListeners();
} catch (e, stack) {
if (e is InsufficientStockException) rethrow;
debugPrint('InventoryViewModel: decreaseStock error: $e');
Sentry.captureException(e, stackTrace: stack);
rethrow;
+3
View File
@@ -114,6 +114,9 @@ class SettingsViewModel extends ChangeNotifier {
Future<void> updateLanguage(AppLanguage language) =>
_save(_settings.copyWith(language: language));
Future<void> updateBarGridRows(int rows) =>
_save(_settings.copyWith(barGridRows: rows.clamp(2, 6).toInt()));
Future<void> _save(AppSettings updated) async {
final previous = _settings;
_settings = updated;
+75 -18
View File
@@ -12,8 +12,10 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import '../models/bar_tab.dart';
import '../models/product.dart';
import '../models/tab_item.dart';
import '../services/product_service.dart';
import '../utils/app_updater.dart';
import '../viewmodels/bar_screen_view_model.dart';
import '../viewmodels/settings_view_model.dart';
import '../l10n/app_localizations.dart';
import '../l10n/app_localizations_helpers.dart';
@@ -38,6 +40,11 @@ class _BarScreenViewState extends State<BarScreenView> {
final l10n = AppLocalizations.of(context);
final viewModel = context.watch<BarScreenViewModel>();
final productsViewModel = context.watch<ProductListViewModel>();
final settings = context.watch<SettingsViewModel>().settings;
final stockByProductId = {
for (final product in productsViewModel.products)
product.id: product.stockQuantity,
};
return Scaffold(
appBar: AppBar(
@@ -118,6 +125,7 @@ class _BarScreenViewState extends State<BarScreenView> {
flex: 2,
child: _ProductGrid(
products: productsViewModel.products,
rows: settings.barGridRows,
hasSelectedTab: viewModel.selectedTab != null,
onProductTap: (product) async {
if (viewModel.selectedTab == null) {
@@ -130,10 +138,18 @@ class _BarScreenViewState extends State<BarScreenView> {
try {
await viewModel.addProductToSelectedTab(product);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (e is! InsufficientStockException) {
Sentry.captureException(e, stackTrace: stack);
}
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.couldNotAddProductToTab)),
SnackBar(
content: Text(
e is InsufficientStockException
? l10n.productOutOfStock
: l10n.couldNotAddProductToTab,
),
),
);
}
},
@@ -146,6 +162,7 @@ class _BarScreenViewState extends State<BarScreenView> {
tabs: viewModel.tabs,
selectedTab: viewModel.selectedTab,
selectedTabId: viewModel.selectedTabId,
stockByProductId: stockByProductId,
onNewTabPressed: () => showNewTabDialog(context),
onTabSelected: viewModel.selectTab,
onItemQuantityChanged: viewModel.changeItemQuantity,
@@ -166,11 +183,13 @@ class _BarScreenViewState extends State<BarScreenView> {
// ---------------------------------------------------------------------------
class _ProductGrid extends StatelessWidget {
final List<Product> products;
final int rows;
final bool hasSelectedTab;
final ValueChanged<Product> onProductTap;
const _ProductGrid({
required this.products,
required this.rows,
required this.hasSelectedTab,
required this.onProductTap,
});
@@ -267,11 +286,26 @@ class _ProductGrid extends StatelessWidget {
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final columns = ((constraints.maxWidth / 170).floor())
.clamp(3, 5)
.toInt();
const horizontalPadding = 32.0;
const verticalPadding = 28.0;
const spacing = 12.0;
final rowCount = rows.clamp(2, 6).toInt();
final rowHeight =
((constraints.maxHeight -
verticalPadding -
(rowCount - 1) * spacing) /
rowCount)
.clamp(96.0, 320.0)
.toDouble();
final columns =
(((constraints.maxWidth - horizontalPadding + spacing) /
(rowHeight + spacing))
.ceil())
.clamp(2, 8)
.toInt();
return GridView.builder(
key: ValueKey('product-grid-$rows'),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@@ -306,6 +340,7 @@ class _TabPanel extends StatefulWidget {
final List<BarTab> tabs;
final BarTab? selectedTab;
final String? selectedTabId;
final Map<String, int> stockByProductId;
final VoidCallback onNewTabPressed;
final ValueChanged<String> onTabSelected;
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
@@ -316,6 +351,7 @@ class _TabPanel extends StatefulWidget {
required this.tabs,
required this.selectedTab,
required this.selectedTabId,
required this.stockByProductId,
required this.onNewTabPressed,
required this.onTabSelected,
required this.onItemQuantityChanged,
@@ -452,6 +488,7 @@ class _TabPanelState extends State<_TabPanel> {
)
: _SelectedTabDetails(
tab: widget.selectedTab!,
stockByProductId: widget.stockByProductId,
onItemQuantityChanged: widget.onItemQuantityChanged,
onCloseTabPressed: widget.onCloseTabPressed,
),
@@ -596,11 +633,13 @@ class _OpenTabsList extends StatelessWidget {
class _SelectedTabDetails extends StatelessWidget {
final BarTab tab;
final Map<String, int> stockByProductId;
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
final VoidCallback onCloseTabPressed;
const _SelectedTabDetails({
required this.tab,
required this.stockByProductId,
required this.onItemQuantityChanged,
required this.onCloseTabPressed,
});
@@ -654,6 +693,7 @@ class _SelectedTabDetails extends StatelessWidget {
return _TabItemRow(
item: item,
stockByProductId: stockByProductId,
onQuantityChanged: onItemQuantityChanged,
);
},
@@ -706,14 +746,21 @@ class _SelectedTabDetails extends StatelessWidget {
class _TabItemRow extends StatelessWidget {
final TabItem item;
final Map<String, int> stockByProductId;
final Future<void> Function(TabItem item, int quantity) onQuantityChanged;
const _TabItemRow({required this.item, required this.onQuantityChanged});
const _TabItemRow({
required this.item,
required this.stockByProductId,
required this.onQuantityChanged,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context);
final availableStock = stockByProductId[item.productId];
final canIncrease = availableStock == null || availableStock > 0;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
@@ -771,18 +818,28 @@ class _TabItemRow extends StatelessWidget {
),
IconButton(
visualDensity: VisualDensity.compact,
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(l10n.couldNotUpdateQuantity)),
);
}
}
},
onPressed: canIncrease
? () async {
try {
await onQuantityChanged(item, item.quantity + 1);
} catch (e, stack) {
if (e is! InsufficientStockException) {
Sentry.captureException(e, stackTrace: stack);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e is InsufficientStockException
? l10n.productOutOfStock
: l10n.couldNotUpdateQuantity,
),
),
);
}
}
}
: null,
icon: const Icon(Icons.add_rounded, size: 18),
),
],
+50 -8
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -22,7 +23,7 @@ class SettingsScreenView extends StatefulWidget {
class _SettingsScreenViewState extends State<SettingsScreenView> {
String _appVersion = '';
int _versionTaps = 0;
Timer? _versionHoldTimer;
@override
void initState() {
@@ -41,13 +42,23 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
return info.version;
}
void _onVersionTap() {
_versionTaps++;
void _startVersionHold() {
_versionHoldTimer?.cancel();
_versionHoldTimer = Timer(const Duration(seconds: 3), () {
_versionHoldTimer = null;
if (mounted) context.push('/dev');
});
}
if (_versionTaps >= 7) {
_versionTaps = 0;
context.push('/dev');
}
void _cancelVersionHold() {
_versionHoldTimer?.cancel();
_versionHoldTimer = null;
}
@override
void dispose() {
_cancelVersionHold();
super.dispose();
}
Future<void> _loadVersion() async {
@@ -277,6 +288,35 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
}
},
),
_SettingsTile(
icon: Icons.grid_view_rounded,
title: l10n.productGridRows,
subtitle: l10n.rowsCount(settings.barGridRows),
onTap: () async {
final selected = await showModalBottomSheet<int>(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final rows in [2, 3, 4, 5, 6])
RadioListTile<int>(
value: rows,
groupValue: settings.barGridRows,
title: Text(l10n.rowsCount(rows)),
onChanged: (value) =>
Navigator.pop(context, value),
),
],
),
),
);
if (selected != null) {
await settingsViewModel.updateBarGridRows(selected);
}
},
),
_SettingsTile(
icon: Icons.brightness_6_rounded,
title: l10n.theme,
@@ -334,7 +374,9 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
),
const SizedBox(height: 40),
GestureDetector(
onTap: _onVersionTap,
onTapDown: (_) => _startVersionHold(),
onTapUp: (_) => _cancelVersionHold(),
onTapCancel: _cancelVersionHold,
child: Center(
child: Text(
l10n.version(_appVersion),