feat: add setting for auto-lock fix: fix sideswipe

This commit is contained in:
2026-08-04 01:09:01 +02:00
parent 0997d110d4
commit 812fcc892b
23 changed files with 629 additions and 28 deletions
+2
View File
@@ -27,6 +27,8 @@ class _KoolTabAppState extends State<KoolTabApp> {
_lifecycleLockObserver = AppLifecycleLockObserver(
onLock: () => context.read<PinLockViewModel>().lock(),
shouldLock: () =>
context.read<SettingsViewModel>().settings.autoLockEnabled,
);
WidgetsBinding.instance.addObserver(_lifecycleLockObserver);
+3 -2
View File
@@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
class AppLifecycleLockObserver extends WidgetsBindingObserver {
final VoidCallback onLock;
final bool Function()? shouldLock;
AppLifecycleLockObserver({required this.onLock});
AppLifecycleLockObserver({required this.onLock, this.shouldLock});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
@@ -12,7 +13,7 @@ class AppLifecycleLockObserver extends WidgetsBindingObserver {
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
onLock();
if (shouldLock?.call() ?? true) onLock();
case AppLifecycleState.resumed:
break;
}
+11 -1
View File
@@ -106,6 +106,9 @@ class AppSettingsTable extends Table {
BoolColumn get pinRequired => boolean().withDefault(const Constant(false))();
BoolColumn get autoLockEnabled =>
boolean().withDefault(const Constant(true))();
TextColumn get themeMode => text().withDefault(const Constant('system'))();
TextColumn get language => text().withDefault(const Constant('system'))();
@@ -132,7 +135,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 8;
int get schemaVersion => 9;
@override
MigrationStrategy get migration {
@@ -181,6 +184,13 @@ class AppDatabase extends _$AppDatabase {
appSettingsTable.barGridRows,
);
}
if (from < 9 && !await hasSettingsColumn('auto_lock_enabled')) {
await migrator.addColumn(
appSettingsTable,
appSettingsTable.autoLockEnabled,
);
}
},
);
}
+82 -2
View File
@@ -2207,6 +2207,21 @@ class $AppSettingsTableTable extends AppSettingsTable
),
defaultValue: const Constant(false),
);
static const VerificationMeta _autoLockEnabledMeta = const VerificationMeta(
'autoLockEnabled',
);
@override
late final GeneratedColumn<bool> autoLockEnabled = GeneratedColumn<bool>(
'auto_lock_enabled',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("auto_lock_enabled" IN (0, 1))',
),
defaultValue: const Constant(true),
);
static const VerificationMeta _themeModeMeta = const VerificationMeta(
'themeMode',
);
@@ -2247,6 +2262,7 @@ class $AppSettingsTableTable extends AppSettingsTable
List<GeneratedColumn> get $columns => [
id,
pinRequired,
autoLockEnabled,
themeMode,
language,
barGridRows,
@@ -2277,6 +2293,15 @@ class $AppSettingsTableTable extends AppSettingsTable
),
);
}
if (data.containsKey('auto_lock_enabled')) {
context.handle(
_autoLockEnabledMeta,
autoLockEnabled.isAcceptableOrUnknown(
data['auto_lock_enabled']!,
_autoLockEnabledMeta,
),
);
}
if (data.containsKey('theme_mode')) {
context.handle(
_themeModeMeta,
@@ -2315,6 +2340,10 @@ class $AppSettingsTableTable extends AppSettingsTable
DriftSqlType.bool,
data['${effectivePrefix}pin_required'],
)!,
autoLockEnabled: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}auto_lock_enabled'],
)!,
themeMode: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}theme_mode'],
@@ -2339,12 +2368,14 @@ class $AppSettingsTableTable extends AppSettingsTable
class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
final String id;
final bool pinRequired;
final bool autoLockEnabled;
final String themeMode;
final String language;
final int barGridRows;
const AppSettingsRow({
required this.id,
required this.pinRequired,
required this.autoLockEnabled,
required this.themeMode,
required this.language,
required this.barGridRows,
@@ -2354,6 +2385,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['pin_required'] = Variable<bool>(pinRequired);
map['auto_lock_enabled'] = Variable<bool>(autoLockEnabled);
map['theme_mode'] = Variable<String>(themeMode);
map['language'] = Variable<String>(language);
map['bar_grid_rows'] = Variable<int>(barGridRows);
@@ -2364,6 +2396,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
return AppSettingsTableCompanion(
id: Value(id),
pinRequired: Value(pinRequired),
autoLockEnabled: Value(autoLockEnabled),
themeMode: Value(themeMode),
language: Value(language),
barGridRows: Value(barGridRows),
@@ -2378,6 +2411,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
return AppSettingsRow(
id: serializer.fromJson<String>(json['id']),
pinRequired: serializer.fromJson<bool>(json['pinRequired']),
autoLockEnabled: serializer.fromJson<bool>(json['autoLockEnabled']),
themeMode: serializer.fromJson<String>(json['themeMode']),
language: serializer.fromJson<String>(json['language']),
barGridRows: serializer.fromJson<int>(json['barGridRows']),
@@ -2389,6 +2423,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'pinRequired': serializer.toJson<bool>(pinRequired),
'autoLockEnabled': serializer.toJson<bool>(autoLockEnabled),
'themeMode': serializer.toJson<String>(themeMode),
'language': serializer.toJson<String>(language),
'barGridRows': serializer.toJson<int>(barGridRows),
@@ -2398,12 +2433,14 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
AppSettingsRow copyWith({
String? id,
bool? pinRequired,
bool? autoLockEnabled,
String? themeMode,
String? language,
int? barGridRows,
}) => AppSettingsRow(
id: id ?? this.id,
pinRequired: pinRequired ?? this.pinRequired,
autoLockEnabled: autoLockEnabled ?? this.autoLockEnabled,
themeMode: themeMode ?? this.themeMode,
language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
@@ -2414,6 +2451,9 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
pinRequired: data.pinRequired.present
? data.pinRequired.value
: this.pinRequired,
autoLockEnabled: data.autoLockEnabled.present
? data.autoLockEnabled.value
: this.autoLockEnabled,
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
language: data.language.present ? data.language.value : this.language,
barGridRows: data.barGridRows.present
@@ -2427,6 +2467,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
return (StringBuffer('AppSettingsRow(')
..write('id: $id, ')
..write('pinRequired: $pinRequired, ')
..write('autoLockEnabled: $autoLockEnabled, ')
..write('themeMode: $themeMode, ')
..write('language: $language, ')
..write('barGridRows: $barGridRows')
@@ -2435,14 +2476,21 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
}
@override
int get hashCode =>
Object.hash(id, pinRequired, themeMode, language, barGridRows);
int get hashCode => Object.hash(
id,
pinRequired,
autoLockEnabled,
themeMode,
language,
barGridRows,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AppSettingsRow &&
other.id == this.id &&
other.pinRequired == this.pinRequired &&
other.autoLockEnabled == this.autoLockEnabled &&
other.themeMode == this.themeMode &&
other.language == this.language &&
other.barGridRows == this.barGridRows);
@@ -2451,6 +2499,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
final Value<String> id;
final Value<bool> pinRequired;
final Value<bool> autoLockEnabled;
final Value<String> themeMode;
final Value<String> language;
final Value<int> barGridRows;
@@ -2458,6 +2507,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
const AppSettingsTableCompanion({
this.id = const Value.absent(),
this.pinRequired = const Value.absent(),
this.autoLockEnabled = const Value.absent(),
this.themeMode = const Value.absent(),
this.language = const Value.absent(),
this.barGridRows = const Value.absent(),
@@ -2466,6 +2516,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
AppSettingsTableCompanion.insert({
required String id,
this.pinRequired = const Value.absent(),
this.autoLockEnabled = const Value.absent(),
this.themeMode = const Value.absent(),
this.language = const Value.absent(),
this.barGridRows = const Value.absent(),
@@ -2474,6 +2525,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
static Insertable<AppSettingsRow> custom({
Expression<String>? id,
Expression<bool>? pinRequired,
Expression<bool>? autoLockEnabled,
Expression<String>? themeMode,
Expression<String>? language,
Expression<int>? barGridRows,
@@ -2482,6 +2534,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
return RawValuesInsertable({
if (id != null) 'id': id,
if (pinRequired != null) 'pin_required': pinRequired,
if (autoLockEnabled != null) 'auto_lock_enabled': autoLockEnabled,
if (themeMode != null) 'theme_mode': themeMode,
if (language != null) 'language': language,
if (barGridRows != null) 'bar_grid_rows': barGridRows,
@@ -2492,6 +2545,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
AppSettingsTableCompanion copyWith({
Value<String>? id,
Value<bool>? pinRequired,
Value<bool>? autoLockEnabled,
Value<String>? themeMode,
Value<String>? language,
Value<int>? barGridRows,
@@ -2500,6 +2554,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
return AppSettingsTableCompanion(
id: id ?? this.id,
pinRequired: pinRequired ?? this.pinRequired,
autoLockEnabled: autoLockEnabled ?? this.autoLockEnabled,
themeMode: themeMode ?? this.themeMode,
language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
@@ -2516,6 +2571,9 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
if (pinRequired.present) {
map['pin_required'] = Variable<bool>(pinRequired.value);
}
if (autoLockEnabled.present) {
map['auto_lock_enabled'] = Variable<bool>(autoLockEnabled.value);
}
if (themeMode.present) {
map['theme_mode'] = Variable<String>(themeMode.value);
}
@@ -2536,6 +2594,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
return (StringBuffer('AppSettingsTableCompanion(')
..write('id: $id, ')
..write('pinRequired: $pinRequired, ')
..write('autoLockEnabled: $autoLockEnabled, ')
..write('themeMode: $themeMode, ')
..write('language: $language, ')
..write('barGridRows: $barGridRows, ')
@@ -4125,6 +4184,7 @@ typedef $$AppSettingsTableTableCreateCompanionBuilder =
AppSettingsTableCompanion Function({
required String id,
Value<bool> pinRequired,
Value<bool> autoLockEnabled,
Value<String> themeMode,
Value<String> language,
Value<int> barGridRows,
@@ -4134,6 +4194,7 @@ typedef $$AppSettingsTableTableUpdateCompanionBuilder =
AppSettingsTableCompanion Function({
Value<String> id,
Value<bool> pinRequired,
Value<bool> autoLockEnabled,
Value<String> themeMode,
Value<String> language,
Value<int> barGridRows,
@@ -4159,6 +4220,11 @@ class $$AppSettingsTableTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get autoLockEnabled => $composableBuilder(
column: $table.autoLockEnabled,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get themeMode => $composableBuilder(
column: $table.themeMode,
builder: (column) => ColumnFilters(column),
@@ -4194,6 +4260,11 @@ class $$AppSettingsTableTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get autoLockEnabled => $composableBuilder(
column: $table.autoLockEnabled,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get themeMode => $composableBuilder(
column: $table.themeMode,
builder: (column) => ColumnOrderings(column),
@@ -4227,6 +4298,11 @@ class $$AppSettingsTableTableAnnotationComposer
builder: (column) => column,
);
GeneratedColumn<bool> get autoLockEnabled => $composableBuilder(
column: $table.autoLockEnabled,
builder: (column) => column,
);
GeneratedColumn<String> get themeMode =>
$composableBuilder(column: $table.themeMode, builder: (column) => column);
@@ -4278,6 +4354,7 @@ class $$AppSettingsTableTableTableManager
({
Value<String> id = const Value.absent(),
Value<bool> pinRequired = const Value.absent(),
Value<bool> autoLockEnabled = const Value.absent(),
Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(),
Value<int> barGridRows = const Value.absent(),
@@ -4285,6 +4362,7 @@ class $$AppSettingsTableTableTableManager
}) => AppSettingsTableCompanion(
id: id,
pinRequired: pinRequired,
autoLockEnabled: autoLockEnabled,
themeMode: themeMode,
language: language,
barGridRows: barGridRows,
@@ -4294,6 +4372,7 @@ class $$AppSettingsTableTableTableManager
({
required String id,
Value<bool> pinRequired = const Value.absent(),
Value<bool> autoLockEnabled = const Value.absent(),
Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(),
Value<int> barGridRows = const Value.absent(),
@@ -4301,6 +4380,7 @@ class $$AppSettingsTableTableTableManager
}) => AppSettingsTableCompanion.insert(
id: id,
pinRequired: pinRequired,
autoLockEnabled: autoLockEnabled,
themeMode: themeMode,
language: language,
barGridRows: barGridRows,
+6
View File
@@ -25,6 +25,7 @@
"settingsTitle": "Settings",
"security": "Security",
"pinRequired": "PIN Required",
"autoLockOnExit": "Auto-lock on phone lock or app exit",
"changePin": "Change PIN",
"appearance": "Appearance",
"language": "Language",
@@ -71,7 +72,12 @@
"selectOrOpenTab": "Select or open a tab",
"noOpenTabs": "No open tabs",
"edit": "Edit",
"editTab": "Edit tab",
"close": "Close",
"deleteTab": "Delete tab?",
"deleteTabDescription": "This will permanently delete the tab and return all its items to stock.",
"couldNotRenameTab": "Could not rename tab.",
"couldNotDeleteTab": "Could not delete tab.",
"tabItemSummary": "{count, plural, =1 {1 item - {total}} other {{count} items - {total}}}",
"@tabItemSummary": {"placeholders": {"count": {"type": "int"}, "total": {"type": "String"}}},
"tabItemCount": "{count, plural, =1 {1 item} other {{count} items}}",
+36
View File
@@ -242,6 +242,12 @@ abstract class AppLocalizations {
/// **'PIN Required'**
String get pinRequired;
/// No description provided for @autoLockOnExit.
///
/// In en, this message translates to:
/// **'Auto-lock on phone lock or app exit'**
String get autoLockOnExit;
/// No description provided for @changePin.
///
/// In en, this message translates to:
@@ -506,12 +512,42 @@ abstract class AppLocalizations {
/// **'Edit'**
String get edit;
/// No description provided for @editTab.
///
/// In en, this message translates to:
/// **'Edit tab'**
String get editTab;
/// No description provided for @close.
///
/// In en, this message translates to:
/// **'Close'**
String get close;
/// No description provided for @deleteTab.
///
/// In en, this message translates to:
/// **'Delete tab?'**
String get deleteTab;
/// No description provided for @deleteTabDescription.
///
/// In en, this message translates to:
/// **'This will permanently delete the tab and return all its items to stock.'**
String get deleteTabDescription;
/// No description provided for @couldNotRenameTab.
///
/// In en, this message translates to:
/// **'Could not rename tab.'**
String get couldNotRenameTab;
/// No description provided for @couldNotDeleteTab.
///
/// In en, this message translates to:
/// **'Could not delete tab.'**
String get couldNotDeleteTab;
/// No description provided for @tabItemSummary.
///
/// In en, this message translates to:
+19
View File
@@ -82,6 +82,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get pinRequired => 'PIN Required';
@override
String get autoLockOnExit => 'Auto-lock on phone lock or app exit';
@override
String get changePin => 'Change PIN';
@@ -219,9 +222,25 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get edit => 'Edit';
@override
String get editTab => 'Edit tab';
@override
String get close => 'Close';
@override
String get deleteTab => 'Delete tab?';
@override
String get deleteTabDescription =>
'This will permanently delete the tab and return all its items to stock.';
@override
String get couldNotRenameTab => 'Could not rename tab.';
@override
String get couldNotDeleteTab => 'Could not delete tab.';
@override
String tabItemSummary(int count, String total) {
String _temp0 = intl.Intl.pluralLogic(
+2
View File
@@ -23,6 +23,8 @@ extension AppLocalizationsHelpers on AppLocalizations {
'Could not add product to tab.' => couldNotAddProductToTab,
'Could not update item quantity.' => couldNotUpdateQuantity,
'Could not close tab.' => couldNotCloseTab,
'Could not rename tab.' => couldNotRenameTab,
'Could not delete tab.' => couldNotDeleteTab,
'Could not reload tabs.' => couldNotReloadTabs,
'Could not load tab history.' => couldNotLoadHistory,
'Could not load more tabs.' => couldNotLoadMoreTabs,
+20
View File
@@ -82,6 +82,10 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get pinRequired => 'PIN vereist';
@override
String get autoLockOnExit =>
'Automatisch vergrendelen bij telefoonslot of afsluiten';
@override
String get changePin => 'PIN wijzigen';
@@ -219,9 +223,25 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get edit => 'Bewerken';
@override
String get editTab => 'Poef bewerken';
@override
String get close => 'Sluiten';
@override
String get deleteTab => 'Poef verwijderen?';
@override
String get deleteTabDescription =>
'Deze poef wordt definitief verwijderd en alle items worden terug aan de voorraad toegevoegd.';
@override
String get couldNotRenameTab => 'Poef kon niet worden hernoemd.';
@override
String get couldNotDeleteTab => 'Poef kon niet worden verwijderd.';
@override
String tabItemSummary(int count, String total) {
String _temp0 = intl.Intl.pluralLogic(
+6
View File
@@ -25,6 +25,7 @@
"settingsTitle": "Instellingen",
"security": "Beveiliging",
"pinRequired": "PIN vereist",
"autoLockOnExit": "Automatisch vergrendelen bij telefoonslot of afsluiten",
"changePin": "PIN wijzigen",
"appearance": "Uiterlijk",
"language": "Taal",
@@ -71,7 +72,12 @@
"selectOrOpenTab": "Selecteer of open een poef",
"noOpenTabs": "Geen open poefs",
"edit": "Bewerken",
"editTab": "Poef bewerken",
"close": "Sluiten",
"deleteTab": "Poef verwijderen?",
"deleteTabDescription": "Deze poef wordt definitief verwijderd en alle items worden terug aan de voorraad toegevoegd.",
"couldNotRenameTab": "Poef kon niet worden hernoemd.",
"couldNotDeleteTab": "Poef kon niet worden verwijderd.",
"tabItemSummary": "{count, plural, =1 {1 item - {total}} other {{count} items - {total}}}",
"@tabItemSummary": {"placeholders": {"count": {"type": "int"}, "total": {"type": "String"}}},
"tabItemCount": "{count, plural, =1 {1 item} other {{count} items}}",
+13 -2
View File
@@ -4,12 +4,14 @@ enum AppLanguage { system, english, dutch }
class AppSettings {
final bool pinRequired;
final bool autoLockEnabled;
final AppThemeMode themeMode;
final AppLanguage language;
final int barGridRows;
const AppSettings({
required this.pinRequired,
this.autoLockEnabled = true,
required this.themeMode,
this.language = AppLanguage.system,
this.barGridRows = 3,
@@ -17,12 +19,14 @@ class AppSettings {
AppSettings copyWith({
bool? pinRequired,
bool? autoLockEnabled,
AppThemeMode? themeMode,
AppLanguage? language,
int? barGridRows,
}) {
return AppSettings(
pinRequired: pinRequired ?? this.pinRequired,
autoLockEnabled: autoLockEnabled ?? this.autoLockEnabled,
themeMode: themeMode ?? this.themeMode,
language: language ?? this.language,
barGridRows: barGridRows ?? this.barGridRows,
@@ -31,6 +35,7 @@ class AppSettings {
static const AppSettings defaults = AppSettings(
pinRequired: false,
autoLockEnabled: true,
themeMode: AppThemeMode.system,
);
@@ -39,11 +44,17 @@ class AppSettings {
identical(this, other) ||
other is AppSettings &&
pinRequired == other.pinRequired &&
autoLockEnabled == other.autoLockEnabled &&
themeMode == other.themeMode &&
language == other.language &&
barGridRows == other.barGridRows;
@override
int get hashCode =>
Object.hash(pinRequired, themeMode, language, barGridRows);
int get hashCode => Object.hash(
pinRequired,
autoLockEnabled,
themeMode,
language,
barGridRows,
);
}
+60
View File
@@ -17,6 +17,11 @@ abstract class BarTabService {
Future<BarTab> createTab({required String customerName});
Future<void> renameTab({required String tabId, required String customerName});
/// Deletes an open tab and returns its items to inventory.
Future<List<TabItem>> deleteTab(String tabId);
Future<void> addProductToTab({
required String tabId,
required Product product,
@@ -203,6 +208,61 @@ class DriftBarTabService implements BarTabService {
return tab;
}
@override
Future<void> renameTab({
required String tabId,
required String customerName,
}) async {
final name = customerName.trim();
if (name.isEmpty) {
throw ArgumentError.value(
customerName,
'customerName',
'Must not be empty',
);
}
final updatedRows =
await (database.update(database.barTabs)
..where((tab) => tab.id.equals(tabId)))
.write(BarTabsCompanion(customerName: Value(name)));
if (updatedRows != 1) {
throw StateError('Tab not found.');
}
}
@override
Future<List<TabItem>> deleteTab(String tabId) async {
return database.transaction(() async {
final tab = await (database.select(
database.barTabs,
)..where((row) => row.id.equals(tabId))).getSingleOrNull();
if (tab == null) return <TabItem>[];
final items = await _getItemsForTab(tabId);
for (final item in items) {
await _increaseProductStock(item.productId, item.quantity);
}
await (database.delete(
database.tabItems,
)..where((item) => item.tabId.equals(tabId))).go();
final deletedRows = await (database.delete(
database.barTabs,
)..where((row) => row.id.equals(tabId))).go();
if (deletedRows != 1) {
throw StateError('Tab was changed before it could be deleted');
}
return items;
});
}
@override
Future<void> addProductToTab({
required String tabId,
+2
View File
@@ -18,6 +18,7 @@ class DriftSettingsService implements SettingsService {
AppSettings _mapRowToSettings(AppSettingsRow row) {
return AppSettings(
pinRequired: row.pinRequired,
autoLockEnabled: row.autoLockEnabled,
themeMode: AppThemeMode.values.firstWhere(
(mode) => mode.name == row.themeMode,
orElse: () => AppThemeMode.system,
@@ -50,6 +51,7 @@ class DriftSettingsService implements SettingsService {
AppSettingsTableCompanion.insert(
id: _settingsId,
pinRequired: Value(settings.pinRequired),
autoLockEnabled: Value(settings.autoLockEnabled),
themeMode: Value(settings.themeMode.name),
language: Value(settings.language.name),
barGridRows: Value(settings.barGridRows),
+31
View File
@@ -90,6 +90,37 @@ class BarScreenViewModel extends ChangeNotifier {
}
}
Future<void> renameTab(String tabId, String customerName) async {
try {
await barTabService.renameTab(tabId: tabId, customerName: customerName);
await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: renameTab error: $e');
_errorMessage = 'Could not rename tab.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
}
Future<void> deleteTab(String tabId) async {
try {
final deletedItems = await barTabService.deleteTab(tabId);
for (final item in deletedItems) {
inventory.applyStockDelta(item.productId, item.quantity);
}
await _reloadTabs();
} catch (e, stack) {
debugPrint('BarScreenViewModel: deleteTab error: $e');
_errorMessage = 'Could not delete tab.';
notifyListeners();
Sentry.captureException(e, stackTrace: stack);
rethrow;
}
}
Future<void> addProductToSelectedTab(Product product) async {
final tab = selectedTab;
+3
View File
@@ -108,6 +108,9 @@ class SettingsViewModel extends ChangeNotifier {
Future<void> updatePinRequired(bool value) =>
_save(_settings.copyWith(pinRequired: value));
Future<void> updateAutoLockEnabled(bool value) =>
_save(_settings.copyWith(autoLockEnabled: value));
Future<void> updateThemeMode(AppThemeMode mode) =>
_save(_settings.copyWith(themeMode: mode));
+17 -21
View File
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:kooltab2/viewmodels/pin_lock_view_model.dart';
import 'package:kooltab2/viewmodels/product_list_view_model.dart';
import 'package:kooltab2/views/dialogs/close_tab_dialog.dart';
import 'package:kooltab2/views/dialogs/edit_tab_dialog.dart';
import 'package:kooltab2/views/dialogs/new_tab_dialog.dart';
import 'package:kooltab2/views/widgets/product_tile.dart';
import 'package:provider/provider.dart';
@@ -167,7 +168,8 @@ class _BarScreenViewState extends State<BarScreenView> {
onTabSelected: viewModel.selectTab,
onItemQuantityChanged: viewModel.changeItemQuantity,
onCloseTabPressed: () => confirmCloseTab(context),
onTabClosed: viewModel.closeTab,
onTabEdited: (tab) => showEditTabDialog(context, tab),
onTabDeleted: (tab) => confirmDeleteTab(context, tab),
),
),
],
@@ -345,7 +347,8 @@ class _TabPanel extends StatefulWidget {
final ValueChanged<String> onTabSelected;
final Future<void> Function(TabItem item, int delta) onItemQuantityChanged;
final VoidCallback onCloseTabPressed;
final Future<void> Function(String) onTabClosed;
final Future<void> Function(BarTab) onTabEdited;
final Future<void> Function(BarTab) onTabDeleted;
const _TabPanel({
required this.tabs,
@@ -356,7 +359,8 @@ class _TabPanel extends StatefulWidget {
required this.onTabSelected,
required this.onItemQuantityChanged,
required this.onCloseTabPressed,
required this.onTabClosed,
required this.onTabEdited,
required this.onTabDeleted,
});
@override
@@ -461,7 +465,8 @@ class _TabPanelState extends State<_TabPanel> {
tabs: filteredTabs,
selectedTabId: widget.selectedTabId,
onTabSelected: widget.onTabSelected,
onTabClosed: widget.onTabClosed,
onTabEdited: widget.onTabEdited,
onTabDeleted: widget.onTabDeleted,
),
),
@@ -504,13 +509,15 @@ class _OpenTabsList extends StatelessWidget {
final List<BarTab> tabs;
final String? selectedTabId;
final ValueChanged<String> onTabSelected;
final Future<void> Function(String) onTabClosed;
final Future<void> Function(BarTab) onTabEdited;
final Future<void> Function(BarTab) onTabDeleted;
const _OpenTabsList({
required this.tabs,
required this.selectedTabId,
required this.onTabSelected,
required this.onTabClosed,
required this.onTabEdited,
required this.onTabDeleted,
});
@override
@@ -543,27 +550,16 @@ class _OpenTabsList extends StatelessWidget {
extentRatio: 0.6,
children: [
SlidableAction(
onPressed: (_) => onTabSelected(tab.id),
onPressed: (_) => onTabEdited(tab),
icon: Icons.edit_outlined,
label: l10n.edit,
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
),
SlidableAction(
onPressed: (_) async {
try {
await onTabClosed(tab.id);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.couldNotCloseTab)),
);
}
}
},
icon: Icons.close_rounded,
label: l10n.close,
onPressed: (_) => onTabDeleted(tab),
icon: Icons.delete_outline_rounded,
label: l10n.delete,
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
+117
View File
@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../l10n/app_localizations.dart';
import '../../models/bar_tab.dart';
import '../../viewmodels/bar_screen_view_model.dart';
Future<void> showEditTabDialog(BuildContext context, BarTab tab) async {
final l10n = AppLocalizations.of(context);
final name = await showDialog<String>(
context: context,
builder: (_) => _EditTabDialog(initialName: tab.customerName),
);
if (name == null || name.trim().isEmpty || !context.mounted) return;
try {
await context.read<BarScreenViewModel>().renameTab(tab.id, name.trim());
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.couldNotRenameTab)));
}
}
class _EditTabDialog extends StatefulWidget {
final String initialName;
const _EditTabDialog({required this.initialName});
@override
State<_EditTabDialog> createState() => _EditTabDialogState();
}
class _EditTabDialogState extends State<_EditTabDialog> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialName);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return AlertDialog(
title: Text(l10n.editTab),
content: TextField(
controller: _controller,
autofocus: true,
textInputAction: TextInputAction.done,
decoration: InputDecoration(labelText: l10n.customerGroupName),
onSubmitted: (value) {
Navigator.of(context).pop(value);
},
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(_controller.text),
child: Text(l10n.confirm),
),
],
);
}
}
Future<void> confirmDeleteTab(BuildContext context, BarTab tab) async {
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(l10n.deleteTab),
content: Text(l10n.deleteTabDescription),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(l10n.delete),
),
],
);
},
);
if (confirmed != true || !context.mounted) return;
try {
await context.read<BarScreenViewModel>().deleteTab(tab.id);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.couldNotDeleteTab)));
}
}
+8
View File
@@ -241,6 +241,14 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
}
},
),
_SettingsSwitchTile(
icon: Icons.lock_clock_rounded,
title: l10n.autoLockOnExit,
value: settings.autoLockEnabled,
onChanged: (value) {
settingsViewModel.updateAutoLockEnabled(value);
},
),
if (settings.pinRequired)
_SettingsTile(
icon: Icons.pin_rounded,
+19
View File
@@ -27,4 +27,23 @@ void main() {
expect(lockCount, 0);
});
test('does not lock when auto-lock is disabled', () {
var lockCount = 0;
final observer = AppLifecycleLockObserver(
onLock: () => lockCount++,
shouldLock: () => false,
);
for (final state in [
AppLifecycleState.inactive,
AppLifecycleState.hidden,
AppLifecycleState.paused,
AppLifecycleState.detached,
]) {
observer.didChangeAppLifecycleState(state);
}
expect(lockCount, 0);
});
}
+33
View File
@@ -1,4 +1,5 @@
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';
@@ -50,4 +51,36 @@ void main() {
() => barTabService.adjustTabItemQuantity(tabItemId: 'item-1', delta: 1),
).called(1);
});
test('updates inventory when a tab is deleted', () async {
final product = Product(
id: 'prod-1',
name: 'Chips',
category: 'Snacks',
stockQuantity: 8,
lowStockThreshold: 2,
priceInCents: 150,
);
const item = TabItem(
id: 'item-1',
tabId: 'tab-1',
productId: 'prod-1',
productName: 'Chips',
quantity: 3,
unitPriceInCents: 150,
);
when(() => productService.getProducts()).thenAnswer((_) async => [product]);
await inventory.load();
when(
() => barTabService.deleteTab('tab-1'),
).thenAnswer((_) async => [item]);
when(() => barTabService.getOpenTabs()).thenAnswer((_) async => []);
await viewModel.deleteTab('tab-1');
expect(inventory.products.first.stockQuantity, 11);
verify(() => barTabService.deleteTab('tab-1')).called(1);
verify(() => barTabService.getOpenTabs()).called(1);
});
}
+34
View File
@@ -136,6 +136,40 @@ void main() {
});
});
group('renameTab', () {
test('updates the tab customer name', () async {
final tab = await service.createTab(customerName: 'John');
await service.renameTab(tabId: tab.id, customerName: ' Jane ');
final updatedTab = await service.getTabById(tab.id);
expect(updatedTab!.customerName, 'Jane');
});
});
group('deleteTab', () {
test('deletes the tab and restores item stock', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
await service.addProductToTab(tabId: tab.id, product: product);
await service.addProductToTab(tabId: tab.id, product: product);
final deletedItems = await service.deleteTab(tab.id);
expect(deletedItems.length, 1);
expect(deletedItems.first.quantity, 2);
expect(await service.getTabById(tab.id), isNull);
final updatedProduct = await (database.select(
database.products,
)..where((row) => row.id.equals(product.id))).getSingle();
expect(updatedProduct.stockQuantity, 100);
final remainingItems = await database.select(database.tabItems).get();
expect(remainingItems, isEmpty);
});
});
group('addProductToTab', () {
test('adds new product to tab', () async {
final tab = await service.createTab(customerName: 'John');
+69
View File
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/l10n/app_localizations.dart';
import 'package:kooltab2/models/bar_tab.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:kooltab2/views/dialogs/edit_tab_dialog.dart';
import 'package:mocktail/mocktail.dart';
import 'package:provider/provider.dart';
class MockBarTabService extends Mock implements BarTabService {}
class MockProductService extends Mock implements ProductService {}
void main() {
testWidgets('edit dialog disposes its controller after closing', (
tester,
) async {
final barTabService = MockBarTabService();
final productService = MockProductService();
final viewModel = BarScreenViewModel(
barTabService: barTabService,
inventory: InventoryViewModel(productService: productService),
);
final tab = BarTab(
id: 'tab-1',
customerName: 'Before',
status: 'open',
openedAt: DateTime(2026),
items: const [],
);
when(
() => barTabService.renameTab(tabId: 'tab-1', customerName: 'After'),
).thenAnswer((_) async {});
when(() => barTabService.getOpenTabs()).thenAnswer((_) async => [tab]);
await tester.pumpWidget(
ChangeNotifierProvider<BarScreenViewModel>.value(
value: viewModel,
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) => Scaffold(
body: ElevatedButton(
onPressed: () => showEditTabDialog(context, tab),
child: const Text('Edit'),
),
),
),
),
),
);
await tester.tap(find.text('Edit'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'After');
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
verify(
() => barTabService.renameTab(tabId: 'tab-1', customerName: 'After'),
).called(1);
});
}
+36
View File
@@ -0,0 +1,36 @@
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/database/app_database.dart';
import 'package:kooltab2/models/settings.dart';
import 'package:kooltab2/services/settings_service.dart';
void main() {
late AppDatabase database;
late DriftSettingsService service;
setUp(() {
database = AppDatabase(NativeDatabase.memory());
service = DriftSettingsService(database: database);
});
tearDown(() async {
await database.close();
});
test('defaults auto-lock to enabled', () async {
final settings = await service.getSettings();
expect(settings, AppSettings.defaults);
expect(settings.autoLockEnabled, isTrue);
});
test('persists the auto-lock setting', () async {
await service.saveSettings(
AppSettings.defaults.copyWith(autoLockEnabled: false),
);
final settings = await service.getSettings();
expect(settings.autoLockEnabled, isFalse);
});
}