feat: add rows count (broken), fix: bug with low stock items
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
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)
|
||||
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 {
|
||||
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(l10n.couldNotUpdateQuantity)),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
e is InsufficientStockException
|
||||
? l10n.productOutOfStock
|
||||
: l10n.couldNotUpdateQuantity,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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++;
|
||||
|
||||
if (_versionTaps >= 7) {
|
||||
_versionTaps = 0;
|
||||
context.push('/dev');
|
||||
void _startVersionHold() {
|
||||
_versionHoldTimer?.cancel();
|
||||
_versionHoldTimer = Timer(const Duration(seconds: 3), () {
|
||||
_versionHoldTimer = null;
|
||||
if (mounted) 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),
|
||||
|
||||
+1
-1
@@ -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
|
||||
# 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.
|
||||
version: 1.0.7+1
|
||||
version: 1.0.9+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -218,7 +218,7 @@ void main() {
|
||||
|
||||
expect(
|
||||
() => service.decreaseStock('low-stock', 5),
|
||||
throwsA(isA<Exception>()),
|
||||
throwsA(isA<InsufficientStockException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user