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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import '../database/app_database.dart'; import '../database/app_database.dart';
import '../services/default_product_seeder.dart'; import '../services/default_product_seeder.dart';
@@ -31,6 +32,9 @@ class _AppBootstrapState extends State<AppBootstrap> {
context.read<BarScreenViewModel>().ensureLoaded(); context.read<BarScreenViewModel>().ensureLoaded();
DefaultExportService(database: database).uploadToServer(); 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:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
part 'app_database.g.dart'; part 'app_database.g.dart';
@DataClassName('ProductRow') @DataClassName('ProductRow')
@@ -111,6 +110,8 @@ class AppSettingsTable extends Table {
TextColumn get language => text().withDefault(const Constant('system'))(); TextColumn get language => text().withDefault(const Constant('system'))();
IntColumn get barGridRows => integer().withDefault(const Constant(3))();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@@ -131,7 +132,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override @override
int get schemaVersion => 7; int get schemaVersion => 8;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@@ -140,6 +141,14 @@ class AppDatabase extends _$AppDatabase {
await migrator.createAll(); await migrator.createAll();
}, },
onUpgrade: (migrator, from, to) async { 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) { if (from < 2) {
await migrator.createTable(barTabs); await migrator.createTable(barTabs);
await migrator.createTable(tabItems); await migrator.createTable(tabItems);
@@ -162,9 +171,16 @@ class AppDatabase extends _$AppDatabase {
await migrator.addColumn(closedTabs, closedTabs.paymentMethod); await migrator.addColumn(closedTabs, closedTabs.paymentMethod);
} }
if (from < 7) { if (from < 7 && !await hasSettingsColumn('language')) {
await migrator.addColumn(appSettingsTable, appSettingsTable.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, requiredDuringInsert: false,
defaultValue: const Constant('system'), defaultValue: const Constant('system'),
); );
static const VerificationMeta _barGridRowsMeta = const VerificationMeta(
'barGridRows',
);
@override @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 @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@@ -2271,6 +2289,15 @@ class $AppSettingsTableTable extends AppSettingsTable
language.isAcceptableOrUnknown(data['language']!, _languageMeta), language.isAcceptableOrUnknown(data['language']!, _languageMeta),
); );
} }
if (data.containsKey('bar_grid_rows')) {
context.handle(
_barGridRowsMeta,
barGridRows.isAcceptableOrUnknown(
data['bar_grid_rows']!,
_barGridRowsMeta,
),
);
}
return context; return context;
} }
@@ -2296,6 +2323,10 @@ class $AppSettingsTableTable extends AppSettingsTable
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}language'], 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 bool pinRequired;
final String themeMode; final String themeMode;
final String language; final String language;
final int barGridRows;
const AppSettingsRow({ const AppSettingsRow({
required this.id, required this.id,
required this.pinRequired, required this.pinRequired,
required this.themeMode, required this.themeMode,
required this.language, required this.language,
required this.barGridRows,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -2323,6 +2356,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
map['pin_required'] = Variable<bool>(pinRequired); map['pin_required'] = Variable<bool>(pinRequired);
map['theme_mode'] = Variable<String>(themeMode); map['theme_mode'] = Variable<String>(themeMode);
map['language'] = Variable<String>(language); map['language'] = Variable<String>(language);
map['bar_grid_rows'] = Variable<int>(barGridRows);
return map; return map;
} }
@@ -2332,6 +2366,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
pinRequired: Value(pinRequired), pinRequired: Value(pinRequired),
themeMode: Value(themeMode), themeMode: Value(themeMode),
language: Value(language), language: Value(language),
barGridRows: Value(barGridRows),
); );
} }
@@ -2345,6 +2380,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
pinRequired: serializer.fromJson<bool>(json['pinRequired']), pinRequired: serializer.fromJson<bool>(json['pinRequired']),
themeMode: serializer.fromJson<String>(json['themeMode']), themeMode: serializer.fromJson<String>(json['themeMode']),
language: serializer.fromJson<String>(json['language']), language: serializer.fromJson<String>(json['language']),
barGridRows: serializer.fromJson<int>(json['barGridRows']),
); );
} }
@override @override
@@ -2355,6 +2391,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
'pinRequired': serializer.toJson<bool>(pinRequired), 'pinRequired': serializer.toJson<bool>(pinRequired),
'themeMode': serializer.toJson<String>(themeMode), 'themeMode': serializer.toJson<String>(themeMode),
'language': serializer.toJson<String>(language), 'language': serializer.toJson<String>(language),
'barGridRows': serializer.toJson<int>(barGridRows),
}; };
} }
@@ -2363,11 +2400,13 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
bool? pinRequired, bool? pinRequired,
String? themeMode, String? themeMode,
String? language, String? language,
int? barGridRows,
}) => AppSettingsRow( }) => AppSettingsRow(
id: id ?? this.id, id: id ?? this.id,
pinRequired: pinRequired ?? this.pinRequired, pinRequired: pinRequired ?? this.pinRequired,
themeMode: themeMode ?? this.themeMode, themeMode: themeMode ?? this.themeMode,
language: language ?? this.language, language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
); );
AppSettingsRow copyWithCompanion(AppSettingsTableCompanion data) { AppSettingsRow copyWithCompanion(AppSettingsTableCompanion data) {
return AppSettingsRow( return AppSettingsRow(
@@ -2377,6 +2416,9 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
: this.pinRequired, : this.pinRequired,
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
language: data.language.present ? data.language.value : this.language, 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('id: $id, ')
..write('pinRequired: $pinRequired, ') ..write('pinRequired: $pinRequired, ')
..write('themeMode: $themeMode, ') ..write('themeMode: $themeMode, ')
..write('language: $language') ..write('language: $language, ')
..write('barGridRows: $barGridRows')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(id, pinRequired, themeMode, language); int get hashCode =>
Object.hash(id, pinRequired, themeMode, language, barGridRows);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -2400,7 +2444,8 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
other.id == this.id && other.id == this.id &&
other.pinRequired == this.pinRequired && other.pinRequired == this.pinRequired &&
other.themeMode == this.themeMode && other.themeMode == this.themeMode &&
other.language == this.language); other.language == this.language &&
other.barGridRows == this.barGridRows);
} }
class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> { class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
@@ -2408,12 +2453,14 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
final Value<bool> pinRequired; final Value<bool> pinRequired;
final Value<String> themeMode; final Value<String> themeMode;
final Value<String> language; final Value<String> language;
final Value<int> barGridRows;
final Value<int> rowid; final Value<int> rowid;
const AppSettingsTableCompanion({ const AppSettingsTableCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.pinRequired = const Value.absent(), this.pinRequired = const Value.absent(),
this.themeMode = const Value.absent(), this.themeMode = const Value.absent(),
this.language = const Value.absent(), this.language = const Value.absent(),
this.barGridRows = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
AppSettingsTableCompanion.insert({ AppSettingsTableCompanion.insert({
@@ -2421,6 +2468,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
this.pinRequired = const Value.absent(), this.pinRequired = const Value.absent(),
this.themeMode = const Value.absent(), this.themeMode = const Value.absent(),
this.language = const Value.absent(), this.language = const Value.absent(),
this.barGridRows = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : id = Value(id); }) : id = Value(id);
static Insertable<AppSettingsRow> custom({ static Insertable<AppSettingsRow> custom({
@@ -2428,6 +2476,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
Expression<bool>? pinRequired, Expression<bool>? pinRequired,
Expression<String>? themeMode, Expression<String>? themeMode,
Expression<String>? language, Expression<String>? language,
Expression<int>? barGridRows,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
@@ -2435,6 +2484,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
if (pinRequired != null) 'pin_required': pinRequired, if (pinRequired != null) 'pin_required': pinRequired,
if (themeMode != null) 'theme_mode': themeMode, if (themeMode != null) 'theme_mode': themeMode,
if (language != null) 'language': language, if (language != null) 'language': language,
if (barGridRows != null) 'bar_grid_rows': barGridRows,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@@ -2444,6 +2494,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
Value<bool>? pinRequired, Value<bool>? pinRequired,
Value<String>? themeMode, Value<String>? themeMode,
Value<String>? language, Value<String>? language,
Value<int>? barGridRows,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return AppSettingsTableCompanion( return AppSettingsTableCompanion(
@@ -2451,6 +2502,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
pinRequired: pinRequired ?? this.pinRequired, pinRequired: pinRequired ?? this.pinRequired,
themeMode: themeMode ?? this.themeMode, themeMode: themeMode ?? this.themeMode,
language: language ?? this.language, language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@@ -2470,6 +2522,9 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
if (language.present) { if (language.present) {
map['language'] = Variable<String>(language.value); map['language'] = Variable<String>(language.value);
} }
if (barGridRows.present) {
map['bar_grid_rows'] = Variable<int>(barGridRows.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@@ -2483,6 +2538,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
..write('pinRequired: $pinRequired, ') ..write('pinRequired: $pinRequired, ')
..write('themeMode: $themeMode, ') ..write('themeMode: $themeMode, ')
..write('language: $language, ') ..write('language: $language, ')
..write('barGridRows: $barGridRows, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@@ -4071,6 +4127,7 @@ typedef $$AppSettingsTableTableCreateCompanionBuilder =
Value<bool> pinRequired, Value<bool> pinRequired,
Value<String> themeMode, Value<String> themeMode,
Value<String> language, Value<String> language,
Value<int> barGridRows,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$AppSettingsTableTableUpdateCompanionBuilder = typedef $$AppSettingsTableTableUpdateCompanionBuilder =
@@ -4079,6 +4136,7 @@ typedef $$AppSettingsTableTableUpdateCompanionBuilder =
Value<bool> pinRequired, Value<bool> pinRequired,
Value<String> themeMode, Value<String> themeMode,
Value<String> language, Value<String> language,
Value<int> barGridRows,
Value<int> rowid, Value<int> rowid,
}); });
@@ -4110,6 +4168,11 @@ class $$AppSettingsTableTableFilterComposer
column: $table.language, column: $table.language,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<int> get barGridRows => $composableBuilder(
column: $table.barGridRows,
builder: (column) => ColumnFilters(column),
);
} }
class $$AppSettingsTableTableOrderingComposer class $$AppSettingsTableTableOrderingComposer
@@ -4140,6 +4203,11 @@ class $$AppSettingsTableTableOrderingComposer
column: $table.language, column: $table.language,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<int> get barGridRows => $composableBuilder(
column: $table.barGridRows,
builder: (column) => ColumnOrderings(column),
);
} }
class $$AppSettingsTableTableAnnotationComposer class $$AppSettingsTableTableAnnotationComposer
@@ -4164,6 +4232,11 @@ class $$AppSettingsTableTableAnnotationComposer
GeneratedColumn<String> get language => GeneratedColumn<String> get language =>
$composableBuilder(column: $table.language, builder: (column) => column); $composableBuilder(column: $table.language, builder: (column) => column);
GeneratedColumn<int> get barGridRows => $composableBuilder(
column: $table.barGridRows,
builder: (column) => column,
);
} }
class $$AppSettingsTableTableTableManager class $$AppSettingsTableTableTableManager
@@ -4207,12 +4280,14 @@ class $$AppSettingsTableTableTableManager
Value<bool> pinRequired = const Value.absent(), Value<bool> pinRequired = const Value.absent(),
Value<String> themeMode = const Value.absent(), Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(), Value<String> language = const Value.absent(),
Value<int> barGridRows = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => AppSettingsTableCompanion( }) => AppSettingsTableCompanion(
id: id, id: id,
pinRequired: pinRequired, pinRequired: pinRequired,
themeMode: themeMode, themeMode: themeMode,
language: language, language: language,
barGridRows: barGridRows,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@@ -4221,12 +4296,14 @@ class $$AppSettingsTableTableTableManager
Value<bool> pinRequired = const Value.absent(), Value<bool> pinRequired = const Value.absent(),
Value<String> themeMode = const Value.absent(), Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(), Value<String> language = const Value.absent(),
Value<int> barGridRows = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => AppSettingsTableCompanion.insert( }) => AppSettingsTableCompanion.insert(
id: id, id: id,
pinRequired: pinRequired, pinRequired: pinRequired,
themeMode: themeMode, themeMode: themeMode,
language: language, language: language,
barGridRows: barGridRows,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
+3
View File
@@ -31,6 +31,9 @@
"languageSystem": "System default", "languageSystem": "System default",
"languageEnglish": "English", "languageEnglish": "English",
"languageDutch": "Dutch", "languageDutch": "Dutch",
"productGridRows": "Product grid rows",
"rowsCount": "{count} rows",
"@rowsCount": {"placeholders": {"count": {"type": "int"}}},
"theme": "Theme", "theme": "Theme",
"system": "System", "system": "System",
"light": "Light", "light": "Light",
+12
View File
@@ -278,6 +278,18 @@ abstract class AppLocalizations {
/// **'Dutch'** /// **'Dutch'**
String get languageDutch; 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. /// No description provided for @theme.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+8
View File
@@ -100,6 +100,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get languageDutch => 'Dutch'; String get languageDutch => 'Dutch';
@override
String get productGridRows => 'Product grid rows';
@override
String rowsCount(int count) {
return '$count rows';
}
@override @override
String get theme => 'Theme'; String get theme => 'Theme';
+8
View File
@@ -100,6 +100,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get languageDutch => 'Nederlands'; String get languageDutch => 'Nederlands';
@override
String get productGridRows => 'Rijen in productraster';
@override
String rowsCount(int count) {
return '$count rijen';
}
@override @override
String get theme => 'Thema'; String get theme => 'Thema';
+3
View File
@@ -31,6 +31,9 @@
"languageSystem": "Systeemstandaard", "languageSystem": "Systeemstandaard",
"languageEnglish": "Engels", "languageEnglish": "Engels",
"languageDutch": "Nederlands", "languageDutch": "Nederlands",
"productGridRows": "Rijen in productraster",
"rowsCount": "{count} rijen",
"@rowsCount": {"placeholders": {"count": {"type": "int"}}},
"theme": "Thema", "theme": "Thema",
"system": "Systeem", "system": "Systeem",
"light": "Licht", "light": "Licht",
+9 -2
View File
@@ -1,26 +1,31 @@
enum AppThemeMode { system, light, dark, ugly } enum AppThemeMode { system, light, dark, ugly }
enum AppLanguage { system, english, dutch } enum AppLanguage { system, english, dutch }
class AppSettings { class AppSettings {
final bool pinRequired; final bool pinRequired;
final AppThemeMode themeMode; final AppThemeMode themeMode;
final AppLanguage language; final AppLanguage language;
final int barGridRows;
const AppSettings({ const AppSettings({
required this.pinRequired, required this.pinRequired,
required this.themeMode, required this.themeMode,
this.language = AppLanguage.system, this.language = AppLanguage.system,
this.barGridRows = 3,
}); });
AppSettings copyWith({ AppSettings copyWith({
bool? pinRequired, bool? pinRequired,
AppThemeMode? themeMode, AppThemeMode? themeMode,
AppLanguage? language, AppLanguage? language,
int? barGridRows,
}) { }) {
return AppSettings( return AppSettings(
pinRequired: pinRequired ?? this.pinRequired, pinRequired: pinRequired ?? this.pinRequired,
themeMode: themeMode ?? this.themeMode, themeMode: themeMode ?? this.themeMode,
language: language ?? this.language, language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
); );
} }
@@ -35,8 +40,10 @@ class AppSettings {
other is AppSettings && other is AppSettings &&
pinRequired == other.pinRequired && pinRequired == other.pinRequired &&
themeMode == other.themeMode && themeMode == other.themeMode &&
language == other.language; language == other.language &&
barGridRows == other.barGridRows;
@override @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 '../database/app_database.dart';
import '../models/product.dart'; import '../models/product.dart';
class InsufficientStockException implements Exception {
const InsufficientStockException();
@override
String toString() => 'Not enough stock';
}
abstract class ProductService { abstract class ProductService {
Future<List<Product>> getProducts(); Future<List<Product>> getProducts();
@@ -130,7 +137,7 @@ class DriftProductService implements ProductService {
} }
if (product.stockQuantity < amount) { if (product.stockQuantity < amount) {
throw Exception('Not enough stock'); throw const InsufficientStockException();
} }
await updateProduct( await updateProduct(
+2
View File
@@ -26,6 +26,7 @@ class DriftSettingsService implements SettingsService {
(language) => language.name == row.language, (language) => language.name == row.language,
orElse: () => AppLanguage.system, orElse: () => AppLanguage.system,
), ),
barGridRows: row.barGridRows.clamp(2, 6).toInt(),
); );
} }
@@ -51,6 +52,7 @@ class DriftSettingsService implements SettingsService {
pinRequired: Value(settings.pinRequired), pinRequired: Value(settings.pinRequired),
themeMode: Value(settings.themeMode.name), themeMode: Value(settings.themeMode.name),
language: Value(settings.language.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/product.dart';
import '../models/tab_item.dart'; import '../models/tab_item.dart';
import '../services/bar_tab_service.dart'; import '../services/bar_tab_service.dart';
import '../services/product_service.dart';
import 'inventory_view_model.dart'; import 'inventory_view_model.dart';
class BarScreenViewModel extends ChangeNotifier { class BarScreenViewModel extends ChangeNotifier {
@@ -95,9 +96,7 @@ class BarScreenViewModel extends ChangeNotifier {
if (tab == null) return; if (tab == null) return;
if (product.stockQuantity <= 0) { if (product.stockQuantity <= 0) {
_errorMessage = 'Product is out of stock.'; throw const InsufficientStockException();
notifyListeners();
return;
} }
try { try {
@@ -109,6 +108,8 @@ class BarScreenViewModel extends ChangeNotifier {
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) { } catch (e, stack) {
if (e is InsufficientStockException) rethrow;
debugPrint('BarScreenViewModel: addProductToSelectedTab error: $e'); debugPrint('BarScreenViewModel: addProductToSelectedTab error: $e');
_errorMessage = 'Could not add product to tab.'; _errorMessage = 'Could not add product to tab.';
notifyListeners(); notifyListeners();
@@ -120,6 +121,20 @@ class BarScreenViewModel extends ChangeNotifier {
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;
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 { try {
await barTabService.updateTabItemQuantity( await barTabService.updateTabItemQuantity(
tabItemId: item.id, tabItemId: item.id,
@@ -135,6 +150,8 @@ class BarScreenViewModel extends ChangeNotifier {
await _reloadTabs(); await _reloadTabs();
} catch (e, stack) { } catch (e, stack) {
if (e is InsufficientStockException) rethrow;
debugPrint('BarScreenViewModel: changeItemQuantity error: $e'); debugPrint('BarScreenViewModel: changeItemQuantity error: $e');
_errorMessage = 'Could not update item quantity.'; _errorMessage = 'Could not update item quantity.';
notifyListeners(); notifyListeners();
+2
View File
@@ -38,6 +38,8 @@ class InventoryViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} catch (e, stack) { } catch (e, stack) {
if (e is InsufficientStockException) rethrow;
debugPrint('InventoryViewModel: decreaseStock error: $e'); debugPrint('InventoryViewModel: decreaseStock error: $e');
Sentry.captureException(e, stackTrace: stack); Sentry.captureException(e, stackTrace: stack);
rethrow; rethrow;
+3
View File
@@ -114,6 +114,9 @@ class SettingsViewModel extends ChangeNotifier {
Future<void> updateLanguage(AppLanguage language) => Future<void> updateLanguage(AppLanguage language) =>
_save(_settings.copyWith(language: 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 { Future<void> _save(AppSettings updated) async {
final previous = _settings; final previous = _settings;
_settings = updated; _settings = updated;
+75 -18
View File
@@ -12,8 +12,10 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import '../models/bar_tab.dart'; import '../models/bar_tab.dart';
import '../models/product.dart'; import '../models/product.dart';
import '../models/tab_item.dart'; import '../models/tab_item.dart';
import '../services/product_service.dart';
import '../utils/app_updater.dart'; import '../utils/app_updater.dart';
import '../viewmodels/bar_screen_view_model.dart'; import '../viewmodels/bar_screen_view_model.dart';
import '../viewmodels/settings_view_model.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../l10n/app_localizations_helpers.dart'; import '../l10n/app_localizations_helpers.dart';
@@ -38,6 +40,11 @@ class _BarScreenViewState extends State<BarScreenView> {
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
final viewModel = context.watch<BarScreenViewModel>(); final viewModel = context.watch<BarScreenViewModel>();
final productsViewModel = context.watch<ProductListViewModel>(); 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -118,6 +125,7 @@ class _BarScreenViewState extends State<BarScreenView> {
flex: 2, flex: 2,
child: _ProductGrid( child: _ProductGrid(
products: productsViewModel.products, products: productsViewModel.products,
rows: settings.barGridRows,
hasSelectedTab: viewModel.selectedTab != null, hasSelectedTab: viewModel.selectedTab != null,
onProductTap: (product) async { onProductTap: (product) async {
if (viewModel.selectedTab == null) { if (viewModel.selectedTab == null) {
@@ -130,10 +138,18 @@ class _BarScreenViewState extends State<BarScreenView> {
try { try {
await viewModel.addProductToSelectedTab(product); await viewModel.addProductToSelectedTab(product);
} catch (e, stack) { } catch (e, stack) {
Sentry.captureException(e, stackTrace: stack); if (e is! InsufficientStockException) {
Sentry.captureException(e, stackTrace: stack);
}
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( 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, tabs: viewModel.tabs,
selectedTab: viewModel.selectedTab, selectedTab: viewModel.selectedTab,
selectedTabId: viewModel.selectedTabId, selectedTabId: viewModel.selectedTabId,
stockByProductId: stockByProductId,
onNewTabPressed: () => showNewTabDialog(context), onNewTabPressed: () => showNewTabDialog(context),
onTabSelected: viewModel.selectTab, onTabSelected: viewModel.selectTab,
onItemQuantityChanged: viewModel.changeItemQuantity, onItemQuantityChanged: viewModel.changeItemQuantity,
@@ -166,11 +183,13 @@ class _BarScreenViewState extends State<BarScreenView> {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
class _ProductGrid extends StatelessWidget { class _ProductGrid extends StatelessWidget {
final List<Product> products; final List<Product> products;
final int rows;
final bool hasSelectedTab; final bool hasSelectedTab;
final ValueChanged<Product> onProductTap; final ValueChanged<Product> onProductTap;
const _ProductGrid({ const _ProductGrid({
required this.products, required this.products,
required this.rows,
required this.hasSelectedTab, required this.hasSelectedTab,
required this.onProductTap, required this.onProductTap,
}); });
@@ -267,11 +286,26 @@ class _ProductGrid extends StatelessWidget {
Expanded( Expanded(
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final columns = ((constraints.maxWidth / 170).floor()) const horizontalPadding = 32.0;
.clamp(3, 5) const verticalPadding = 28.0;
.toInt(); 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( return GridView.builder(
key: ValueKey('product-grid-$rows'),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
itemCount: products.length, itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@@ -306,6 +340,7 @@ class _TabPanel extends StatefulWidget {
final List<BarTab> tabs; final List<BarTab> tabs;
final BarTab? selectedTab; final BarTab? selectedTab;
final String? selectedTabId; final String? selectedTabId;
final Map<String, int> stockByProductId;
final VoidCallback onNewTabPressed; final VoidCallback onNewTabPressed;
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;
@@ -316,6 +351,7 @@ class _TabPanel extends StatefulWidget {
required this.tabs, required this.tabs,
required this.selectedTab, required this.selectedTab,
required this.selectedTabId, required this.selectedTabId,
required this.stockByProductId,
required this.onNewTabPressed, required this.onNewTabPressed,
required this.onTabSelected, required this.onTabSelected,
required this.onItemQuantityChanged, required this.onItemQuantityChanged,
@@ -452,6 +488,7 @@ class _TabPanelState extends State<_TabPanel> {
) )
: _SelectedTabDetails( : _SelectedTabDetails(
tab: widget.selectedTab!, tab: widget.selectedTab!,
stockByProductId: widget.stockByProductId,
onItemQuantityChanged: widget.onItemQuantityChanged, onItemQuantityChanged: widget.onItemQuantityChanged,
onCloseTabPressed: widget.onCloseTabPressed, onCloseTabPressed: widget.onCloseTabPressed,
), ),
@@ -596,11 +633,13 @@ class _OpenTabsList extends StatelessWidget {
class _SelectedTabDetails extends StatelessWidget { class _SelectedTabDetails extends StatelessWidget {
final BarTab tab; final BarTab tab;
final Map<String, int> stockByProductId;
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged; final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
final VoidCallback onCloseTabPressed; final VoidCallback onCloseTabPressed;
const _SelectedTabDetails({ const _SelectedTabDetails({
required this.tab, required this.tab,
required this.stockByProductId,
required this.onItemQuantityChanged, required this.onItemQuantityChanged,
required this.onCloseTabPressed, required this.onCloseTabPressed,
}); });
@@ -654,6 +693,7 @@ class _SelectedTabDetails extends StatelessWidget {
return _TabItemRow( return _TabItemRow(
item: item, item: item,
stockByProductId: stockByProductId,
onQuantityChanged: onItemQuantityChanged, onQuantityChanged: onItemQuantityChanged,
); );
}, },
@@ -706,14 +746,21 @@ class _SelectedTabDetails extends StatelessWidget {
class _TabItemRow extends StatelessWidget { class _TabItemRow extends StatelessWidget {
final TabItem item; final TabItem item;
final Map<String, int> stockByProductId;
final Future<void> Function(TabItem item, int quantity) onQuantityChanged; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
final availableStock = stockByProductId[item.productId];
final canIncrease = availableStock == null || availableStock > 0;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
@@ -771,18 +818,28 @@ class _TabItemRow extends StatelessWidget {
), ),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () async { onPressed: canIncrease
try { ? () async {
await onQuantityChanged(item, item.quantity + 1); try {
} catch (e, stack) { await onQuantityChanged(item, item.quantity + 1);
Sentry.captureException(e, stackTrace: stack); } catch (e, stack) {
if (context.mounted) { if (e is! InsufficientStockException) {
ScaffoldMessenger.of(context).showSnackBar( Sentry.captureException(e, stackTrace: stack);
SnackBar(content: Text(l10n.couldNotUpdateQuantity)), }
); 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), icon: const Icon(Icons.add_rounded, size: 18),
), ),
], ],
+50 -8
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -22,7 +23,7 @@ class SettingsScreenView extends StatefulWidget {
class _SettingsScreenViewState extends State<SettingsScreenView> { class _SettingsScreenViewState extends State<SettingsScreenView> {
String _appVersion = ''; String _appVersion = '';
int _versionTaps = 0; Timer? _versionHoldTimer;
@override @override
void initState() { void initState() {
@@ -41,13 +42,23 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
return info.version; return info.version;
} }
void _onVersionTap() { void _startVersionHold() {
_versionTaps++; _versionHoldTimer?.cancel();
_versionHoldTimer = Timer(const Duration(seconds: 3), () {
_versionHoldTimer = null;
if (mounted) context.push('/dev');
});
}
if (_versionTaps >= 7) { void _cancelVersionHold() {
_versionTaps = 0; _versionHoldTimer?.cancel();
context.push('/dev'); _versionHoldTimer = null;
} }
@override
void dispose() {
_cancelVersionHold();
super.dispose();
} }
Future<void> _loadVersion() async { 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( _SettingsTile(
icon: Icons.brightness_6_rounded, icon: Icons.brightness_6_rounded,
title: l10n.theme, title: l10n.theme,
@@ -334,7 +374,9 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
GestureDetector( GestureDetector(
onTap: _onVersionTap, onTapDown: (_) => _startVersionHold(),
onTapUp: (_) => _cancelVersionHold(),
onTapCancel: _cancelVersionHold,
child: Center( child: Center(
child: Text( child: Text(
l10n.version(_appVersion), l10n.version(_appVersion),
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.7+1 version: 1.0.9+1
environment: environment:
sdk: ^3.12.2 sdk: ^3.12.2
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/models/product.dart';
import 'package:kooltab2/models/tab_item.dart';
import 'package:kooltab2/services/bar_tab_service.dart';
import 'package:kooltab2/services/product_service.dart';
import 'package:kooltab2/viewmodels/bar_screen_view_model.dart';
import 'package:kooltab2/viewmodels/inventory_view_model.dart';
import 'package:mocktail/mocktail.dart';
class MockBarTabService extends Mock implements BarTabService {}
class MockProductService extends Mock implements ProductService {}
void main() {
late MockBarTabService barTabService;
late MockProductService productService;
late InventoryViewModel inventory;
late BarScreenViewModel viewModel;
setUp(() {
barTabService = MockBarTabService();
productService = MockProductService();
inventory = InventoryViewModel(productService: productService);
viewModel = BarScreenViewModel(
barTabService: barTabService,
inventory: inventory,
);
});
test(
'does not increase a tab item when its product is out of stock',
() async {
const product = Product(
id: 'prod-1',
name: 'Chips',
category: 'Snacks',
stockQuantity: 0,
lowStockThreshold: 5,
priceInCents: 150,
);
const item = TabItem(
id: 'item-1',
tabId: 'tab-1',
productId: 'prod-1',
productName: 'Chips',
quantity: 20,
unitPriceInCents: 150,
);
when(
() => productService.getProducts(),
).thenAnswer((_) async => [product]);
await inventory.load();
await expectLater(
viewModel.changeItemQuantity(item, 21),
throwsA(isA<InsufficientStockException>()),
);
expect(viewModel.errorMessage, isNull);
verifyNoMoreInteractions(barTabService);
},
);
}
+1 -1
View File
@@ -218,7 +218,7 @@ void main() {
expect( expect(
() => service.decreaseStock('low-stock', 5), () => service.decreaseStock('low-stock', 5),
throwsA(isA<Exception>()), throwsA(isA<InsufficientStockException>()),
); );
}); });
}); });