From 3cf4787e8c2c0319378cb13bc3e9d47f4f54c483 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Wed, 29 Jul 2026 21:25:56 +0200 Subject: [PATCH] feat: add payment method --- lib/app/app.dart | 11 +- lib/database/app_database.dart | 8 +- lib/database/app_database.g.dart | 78 +++++- lib/models/closed_tab.dart | 14 +- lib/models/settings.dart | 2 +- lib/services/bar_tab_service.dart | 6 +- lib/theme.dart | 319 ++++++++++++++++++++++ lib/viewmodels/bar_screen_view_model.dart | 8 +- lib/viewmodels/dev_menu_view_model.dart | 3 + lib/views/dialogs/close_tab_dialog.dart | 128 +++++++-- lib/views/settings_view.dart | 2 + lib/views/widgets/closed_tab_card.dart | 63 ++++- pubspec.lock | 56 ++++ pubspec.yaml | 4 + 14 files changed, 652 insertions(+), 50 deletions(-) diff --git a/lib/app/app.dart b/lib/app/app.dart index b82167e..607b6f7 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -34,7 +34,7 @@ class _KoolTabAppState extends State { debugShowCheckedModeBanner: false, routerConfig: widget.router, theme: lightTheme, - darkTheme: darkTheme, + darkTheme: _themeFor(settings.themeMode), themeMode: _mapThemeMode(settings.themeMode, context), ); } @@ -45,8 +45,17 @@ class _KoolTabAppState extends State { return switch (mode) { AppThemeMode.dark => ThemeMode.dark, AppThemeMode.light => ThemeMode.light, + AppThemeMode.ugly => ThemeMode.dark, AppThemeMode.system => hasLoaded ? ThemeMode.system : ThemeMode.dark, }; } + + + ThemeData _themeFor(AppThemeMode mode) { + return switch (mode) { + AppThemeMode.ugly => uglyTheme, + _ => darkTheme, + }; + } } \ No newline at end of file diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index d2f1fd9..a9cfcba 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -73,6 +73,8 @@ class ClosedTabs extends Table { DateTimeColumn get closedAt => dateTime()(); + TextColumn get paymentMethod => text().withDefault(const Constant('cash'))(); + @override Set get primaryKey => {id}; } @@ -126,7 +128,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 5; + int get schemaVersion => 6; @override MigrationStrategy get migration { @@ -152,6 +154,10 @@ class AppDatabase extends _$AppDatabase { if (from < 5) { await migrator.createTable(appSettingsTable); } + + if (from < 6) { + await migrator.addColumn(closedTabs, closedTabs.paymentMethod); + } }, ); } diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 6795b93..7c62f3e 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -1417,12 +1417,25 @@ class $ClosedTabsTable extends ClosedTabs type: DriftSqlType.dateTime, requiredDuringInsert: true, ); + static const VerificationMeta _paymentMethodMeta = const VerificationMeta( + 'paymentMethod', + ); + @override + late final GeneratedColumn paymentMethod = GeneratedColumn( + 'payment_method', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('cash'), + ); @override List get $columns => [ id, originalTabId, customerName, closedAt, + paymentMethod, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -1471,6 +1484,15 @@ class $ClosedTabsTable extends ClosedTabs } else if (isInserting) { context.missing(_closedAtMeta); } + if (data.containsKey('payment_method')) { + context.handle( + _paymentMethodMeta, + paymentMethod.isAcceptableOrUnknown( + data['payment_method']!, + _paymentMethodMeta, + ), + ); + } return context; } @@ -1496,6 +1518,10 @@ class $ClosedTabsTable extends ClosedTabs DriftSqlType.dateTime, data['${effectivePrefix}closed_at'], )!, + paymentMethod: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_method'], + )!, ); } @@ -1510,11 +1536,13 @@ class ClosedTabsRow extends DataClass implements Insertable { final String originalTabId; final String customerName; final DateTime closedAt; + final String paymentMethod; const ClosedTabsRow({ required this.id, required this.originalTabId, required this.customerName, required this.closedAt, + required this.paymentMethod, }); @override Map toColumns(bool nullToAbsent) { @@ -1523,6 +1551,7 @@ class ClosedTabsRow extends DataClass implements Insertable { map['original_tab_id'] = Variable(originalTabId); map['customer_name'] = Variable(customerName); map['closed_at'] = Variable(closedAt); + map['payment_method'] = Variable(paymentMethod); return map; } @@ -1532,6 +1561,7 @@ class ClosedTabsRow extends DataClass implements Insertable { originalTabId: Value(originalTabId), customerName: Value(customerName), closedAt: Value(closedAt), + paymentMethod: Value(paymentMethod), ); } @@ -1545,6 +1575,7 @@ class ClosedTabsRow extends DataClass implements Insertable { originalTabId: serializer.fromJson(json['originalTabId']), customerName: serializer.fromJson(json['customerName']), closedAt: serializer.fromJson(json['closedAt']), + paymentMethod: serializer.fromJson(json['paymentMethod']), ); } @override @@ -1555,6 +1586,7 @@ class ClosedTabsRow extends DataClass implements Insertable { 'originalTabId': serializer.toJson(originalTabId), 'customerName': serializer.toJson(customerName), 'closedAt': serializer.toJson(closedAt), + 'paymentMethod': serializer.toJson(paymentMethod), }; } @@ -1563,11 +1595,13 @@ class ClosedTabsRow extends DataClass implements Insertable { String? originalTabId, String? customerName, DateTime? closedAt, + String? paymentMethod, }) => ClosedTabsRow( id: id ?? this.id, originalTabId: originalTabId ?? this.originalTabId, customerName: customerName ?? this.customerName, closedAt: closedAt ?? this.closedAt, + paymentMethod: paymentMethod ?? this.paymentMethod, ); ClosedTabsRow copyWithCompanion(ClosedTabsCompanion data) { return ClosedTabsRow( @@ -1579,6 +1613,9 @@ class ClosedTabsRow extends DataClass implements Insertable { ? data.customerName.value : this.customerName, closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt, + paymentMethod: data.paymentMethod.present + ? data.paymentMethod.value + : this.paymentMethod, ); } @@ -1588,13 +1625,15 @@ class ClosedTabsRow extends DataClass implements Insertable { ..write('id: $id, ') ..write('originalTabId: $originalTabId, ') ..write('customerName: $customerName, ') - ..write('closedAt: $closedAt') + ..write('closedAt: $closedAt, ') + ..write('paymentMethod: $paymentMethod') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, originalTabId, customerName, closedAt); + int get hashCode => + Object.hash(id, originalTabId, customerName, closedAt, paymentMethod); @override bool operator ==(Object other) => identical(this, other) || @@ -1602,7 +1641,8 @@ class ClosedTabsRow extends DataClass implements Insertable { other.id == this.id && other.originalTabId == this.originalTabId && other.customerName == this.customerName && - other.closedAt == this.closedAt); + other.closedAt == this.closedAt && + other.paymentMethod == this.paymentMethod); } class ClosedTabsCompanion extends UpdateCompanion { @@ -1610,12 +1650,14 @@ class ClosedTabsCompanion extends UpdateCompanion { final Value originalTabId; final Value customerName; final Value closedAt; + final Value paymentMethod; final Value rowid; const ClosedTabsCompanion({ this.id = const Value.absent(), this.originalTabId = const Value.absent(), this.customerName = const Value.absent(), this.closedAt = const Value.absent(), + this.paymentMethod = const Value.absent(), this.rowid = const Value.absent(), }); ClosedTabsCompanion.insert({ @@ -1623,6 +1665,7 @@ class ClosedTabsCompanion extends UpdateCompanion { required String originalTabId, required String customerName, required DateTime closedAt, + this.paymentMethod = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), originalTabId = Value(originalTabId), @@ -1633,6 +1676,7 @@ class ClosedTabsCompanion extends UpdateCompanion { Expression? originalTabId, Expression? customerName, Expression? closedAt, + Expression? paymentMethod, Expression? rowid, }) { return RawValuesInsertable({ @@ -1640,6 +1684,7 @@ class ClosedTabsCompanion extends UpdateCompanion { if (originalTabId != null) 'original_tab_id': originalTabId, if (customerName != null) 'customer_name': customerName, if (closedAt != null) 'closed_at': closedAt, + if (paymentMethod != null) 'payment_method': paymentMethod, if (rowid != null) 'rowid': rowid, }); } @@ -1649,6 +1694,7 @@ class ClosedTabsCompanion extends UpdateCompanion { Value? originalTabId, Value? customerName, Value? closedAt, + Value? paymentMethod, Value? rowid, }) { return ClosedTabsCompanion( @@ -1656,6 +1702,7 @@ class ClosedTabsCompanion extends UpdateCompanion { originalTabId: originalTabId ?? this.originalTabId, customerName: customerName ?? this.customerName, closedAt: closedAt ?? this.closedAt, + paymentMethod: paymentMethod ?? this.paymentMethod, rowid: rowid ?? this.rowid, ); } @@ -1675,6 +1722,9 @@ class ClosedTabsCompanion extends UpdateCompanion { if (closedAt.present) { map['closed_at'] = Variable(closedAt.value); } + if (paymentMethod.present) { + map['payment_method'] = Variable(paymentMethod.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -1688,6 +1738,7 @@ class ClosedTabsCompanion extends UpdateCompanion { ..write('originalTabId: $originalTabId, ') ..write('customerName: $customerName, ') ..write('closedAt: $closedAt, ') + ..write('paymentMethod: $paymentMethod, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -3536,6 +3587,7 @@ typedef $$ClosedTabsTableCreateCompanionBuilder = required String originalTabId, required String customerName, required DateTime closedAt, + Value paymentMethod, Value rowid, }); typedef $$ClosedTabsTableUpdateCompanionBuilder = @@ -3544,6 +3596,7 @@ typedef $$ClosedTabsTableUpdateCompanionBuilder = Value originalTabId, Value customerName, Value closedAt, + Value paymentMethod, Value rowid, }); @@ -3575,6 +3628,11 @@ class $$ClosedTabsTableFilterComposer column: $table.closedAt, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => ColumnFilters(column), + ); } class $$ClosedTabsTableOrderingComposer @@ -3605,6 +3663,11 @@ class $$ClosedTabsTableOrderingComposer column: $table.closedAt, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => ColumnOrderings(column), + ); } class $$ClosedTabsTableAnnotationComposer @@ -3631,6 +3694,11 @@ class $$ClosedTabsTableAnnotationComposer GeneratedColumn get closedAt => $composableBuilder(column: $table.closedAt, builder: (column) => column); + + GeneratedColumn get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => column, + ); } class $$ClosedTabsTableTableManager @@ -3668,12 +3736,14 @@ class $$ClosedTabsTableTableManager Value originalTabId = const Value.absent(), Value customerName = const Value.absent(), Value closedAt = const Value.absent(), + Value paymentMethod = const Value.absent(), Value rowid = const Value.absent(), }) => ClosedTabsCompanion( id: id, originalTabId: originalTabId, customerName: customerName, closedAt: closedAt, + paymentMethod: paymentMethod, rowid: rowid, ), createCompanionCallback: @@ -3682,12 +3752,14 @@ class $$ClosedTabsTableTableManager required String originalTabId, required String customerName, required DateTime closedAt, + Value paymentMethod = const Value.absent(), Value rowid = const Value.absent(), }) => ClosedTabsCompanion.insert( id: id, originalTabId: originalTabId, customerName: customerName, closedAt: closedAt, + paymentMethod: paymentMethod, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/models/closed_tab.dart b/lib/models/closed_tab.dart index b6c14e9..9c2d60c 100644 --- a/lib/models/closed_tab.dart +++ b/lib/models/closed_tab.dart @@ -7,6 +7,7 @@ class ClosedTab { final String originalTabId; final String customerName; final DateTime closedAt; + final String paymentMethod; final List items; ClosedTab({ @@ -14,6 +15,7 @@ class ClosedTab { required this.originalTabId, required this.customerName, required this.closedAt, + required this.paymentMethod, required this.items, }); @@ -25,6 +27,14 @@ class ClosedTab { String get formattedTotal => NumberFormat.simpleCurrency().format(totalInCents / 100); + String get formattedPaymentMethod { + return switch (paymentMethod) { + 'cash' => 'Cash', + 'payconiq' => 'Payconiq', + _ => 'Cash', + }; + } + @override bool operator ==(Object other) => identical(this, other) || @@ -33,6 +43,7 @@ class ClosedTab { originalTabId == other.originalTabId && customerName == other.customerName && closedAt == other.closedAt && + paymentMethod == other.paymentMethod && _listEquals(items, other.items); @override @@ -41,6 +52,7 @@ class ClosedTab { originalTabId, customerName, closedAt, + paymentMethod, Object.hashAll(items), ); } @@ -53,4 +65,4 @@ bool _listEquals(List? a, List? b) { if (a[i] != b[i]) return false; } return true; -} +} \ No newline at end of file diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 965e1a8..e3836fb 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,4 +1,4 @@ -enum AppThemeMode { system, light, dark } +enum AppThemeMode { system, light, dark, ugly } class AppSettings { final bool pinRequired; diff --git a/lib/services/bar_tab_service.dart b/lib/services/bar_tab_service.dart index eaae987..63e8f0f 100644 --- a/lib/services/bar_tab_service.dart +++ b/lib/services/bar_tab_service.dart @@ -29,7 +29,7 @@ abstract class BarTabService { /// Archives the tab's current items into history and clears them. /// The tab itself stays open under the same customer name. - Future closeTab(String tabId); + Future closeTab(String tabId, {String paymentMethod = 'cash'}); Future> getClosedTabs(); @@ -234,7 +234,7 @@ class DriftBarTabService implements BarTabService { } @override - Future closeTab(String tabId) async { + Future closeTab(String tabId, {String paymentMethod = 'cash'}) async { await database.transaction(() async { final tabQuery = database.select(database.barTabs) ..where((tab) => tab.id.equals(tabId)); @@ -261,6 +261,7 @@ class DriftBarTabService implements BarTabService { originalTabId: tabId, customerName: tabRow.customerName, closedAt: DateTime.now(), + paymentMethod: Value(paymentMethod), ), ); @@ -325,6 +326,7 @@ class DriftBarTabService implements BarTabService { originalTabId: row.originalTabId, customerName: row.customerName, closedAt: row.closedAt, + paymentMethod: row.paymentMethod, items: itemRows.map(_mapClosedItemRow).toList(), ), ); diff --git a/lib/theme.dart b/lib/theme.dart index 40cd273..8a1cdf7 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -727,3 +727,322 @@ final ThemeData lightTheme = ThemeData( ), ), ); + +// --------------------------------------------------------------------------- +// Ugly theme — intentionally repulsive +// --------------------------------------------------------------------------- + +const _uglyFg = Color(0xFFFFF700); +const _uglyBg = Color(0xFF00FFFF); +const _uglySurface = Color(0xFFFF00FF); +const _uglyPrimary = Color(0xFFCCFF00); +const _uglySecondary = Color(0xFFFF1493); +const _uglyError = Color(0xFFFF6600); + +final ThemeData uglyTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.light, + + colorScheme: const ColorScheme.light( + primary: _uglyPrimary, + secondary: _uglySecondary, + surface: _uglySurface, + error: _uglyError, + onPrimary: Color(0xFF0000FF), + onSecondary: _uglyFg, + onSurface: _uglyFg, + onError: Color(0xFF000000), + onSurfaceVariant: Color(0xFFFF0000), + ), + + scaffoldBackgroundColor: _uglyBg, + canvasColor: _uglyBg, + + appBarTheme: const AppBarTheme( + elevation: 8, + shadowColor: Color(0xFFFF0000), + centerTitle: true, + backgroundColor: _uglySecondary, + foregroundColor: _uglyFg, + surfaceTintColor: _uglyError, + titleTextStyle: TextStyle( + fontSize: 30, + fontWeight: FontWeight.w900, + letterSpacing: 4, + color: _uglyFg, + fontFamily: 'monospace', + ), + iconTheme: IconThemeData(color: _uglyBg, size: 30), + actionsIconTheme: IconThemeData(color: _uglyFg, size: 30), + toolbarHeight: 80, + ), + + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + backgroundColor: _uglyError, + foregroundColor: Colors.black, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(0), + ), + ), + side: const BorderSide(color: _uglyPrimary, width: 3), + padding: const EdgeInsets.all(16), + ), + ), + + cardTheme: CardThemeData( + color: _uglySurface, + elevation: 8, + shadowColor: _uglyPrimary, + margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(30), + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(4), + ), + side: const BorderSide(color: _uglySecondary, width: 4), + ), + ), + + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + elevation: 4, + backgroundColor: _uglyPrimary, + foregroundColor: const Color(0xFF0000FF), + shadowColor: _uglyError, + disabledBackgroundColor: _uglyBg, + disabledForegroundColor: _uglyError, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 22), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2), + side: const BorderSide(color: _uglySecondary, width: 3), + ), + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + letterSpacing: 3, + fontFamily: 'monospace', + ), + ), + ), + + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 6, + backgroundColor: _uglyError, + foregroundColor: _uglyBg, + shadowColor: _uglyPrimary, + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + side: const BorderSide(color: _uglyFg, width: 4), + ), + textStyle: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w900, + fontFamily: 'monospace', + ), + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: _uglyFg, + side: const BorderSide(color: _uglySecondary, width: 5), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)), + textStyle: const TextStyle( + fontWeight: FontWeight.w900, + fontFamily: 'monospace', + ), + ), + ), + + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: _uglyError, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + textStyle: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), + ), + + inputDecorationTheme: const InputDecorationTheme( + filled: true, + fillColor: Color(0xFFAAFFAA), + hintStyle: TextStyle( + color: Color(0xFFFF0000), + fontWeight: FontWeight.w900, + fontStyle: FontStyle.italic, + fontFamily: 'monospace', + ), + contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + borderSide: BorderSide(color: _uglySecondary, width: 4), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + borderSide: BorderSide(color: _uglyError, width: 4), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + borderSide: BorderSide(color: _uglyPrimary, width: 6), + ), + ), + + dialogTheme: DialogThemeData( + backgroundColor: _uglySurface, + elevation: 16, + shadowColor: _uglyFg, + surfaceTintColor: _uglyError, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + side: const BorderSide(color: _uglyPrimary, width: 5), + ), + titleTextStyle: const TextStyle( + color: _uglyFg, + fontSize: 26, + fontWeight: FontWeight.w900, + fontFamily: 'monospace', + ), + contentTextStyle: const TextStyle( + color: _uglyBg, + fontWeight: FontWeight.w700, + height: 1.8, + ), + ), + + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: _uglySecondary, + foregroundColor: _uglyFg, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + side: BorderSide(color: _uglyPrimary, width: 4), + ), + ), + + listTileTheme: ListTileThemeData( + tileColor: _uglyFg, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + side: const BorderSide(color: _uglySecondary, width: 3), + ), + iconColor: _uglyBg, + textColor: _uglySurface, + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + ), + + snackBarTheme: SnackBarThemeData( + backgroundColor: _uglyPrimary, + behavior: SnackBarBehavior.floating, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + side: const BorderSide(color: _uglySecondary, width: 4), + ), + contentTextStyle: const TextStyle( + color: Color(0xFF0000FF), + fontWeight: FontWeight.w900, + fontFamily: 'monospace', + ), + ), + + dividerTheme: const DividerThemeData( + color: _uglySecondary, + thickness: 5, + space: 8, + ), + + chipTheme: const ChipThemeData( + backgroundColor: _uglyError, + labelStyle: TextStyle( + color: _uglyFg, + fontWeight: FontWeight.w900, + fontSize: 14, + fontFamily: 'monospace', + ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), + side: BorderSide(color: _uglyPrimary, width: 3), + ), + ), + + textTheme: const TextTheme( + displayLarge: TextStyle( + color: _uglyFg, + fontWeight: FontWeight.w900, + fontSize: 52, + letterSpacing: 5, + fontFamily: 'monospace', + ), + headlineMedium: TextStyle( + color: _uglyPrimary, + fontWeight: FontWeight.w900, + fontSize: 34, + letterSpacing: 3, + fontFamily: 'monospace', + ), + titleLarge: TextStyle( + color: _uglyFg, + fontWeight: FontWeight.w900, + fontSize: 26, + letterSpacing: 2, + fontFamily: 'monospace', + ), + titleMedium: TextStyle( + color: _uglyPrimary, + fontWeight: FontWeight.w900, + fontSize: 20, + fontFamily: 'monospace', + ), + bodyLarge: TextStyle( + color: _uglyFg, + fontSize: 18, + fontWeight: FontWeight.w700, + height: 1.8, + fontFamily: 'monospace', + ), + bodyMedium: TextStyle( + color: _uglyBg, + fontWeight: FontWeight.w700, + fontSize: 14, + fontFamily: 'monospace', + ), + bodySmall: TextStyle( + color: _uglyError, + fontWeight: FontWeight.w900, + fontSize: 12, + fontFamily: 'monospace', + ), + labelLarge: TextStyle( + color: _uglySecondary, + fontWeight: FontWeight.w900, + fontSize: 16, + fontFamily: 'monospace', + ), + ), +); diff --git a/lib/viewmodels/bar_screen_view_model.dart b/lib/viewmodels/bar_screen_view_model.dart index cc984cf..2cfb85a 100644 --- a/lib/viewmodels/bar_screen_view_model.dart +++ b/lib/viewmodels/bar_screen_view_model.dart @@ -115,18 +115,18 @@ class BarScreenViewModel extends ChangeNotifier { await _reloadTabs(); } - Future closeSelectedTab() async { + Future closeSelectedTab({String paymentMethod = 'cash'}) async { final tab = selectedTab; if (tab == null) return; - await barTabService.closeTab(tab.id); + await barTabService.closeTab(tab.id, paymentMethod: paymentMethod); await _reloadTabs(); } - Future closeTab(String tabId) async { - await barTabService.closeTab(tabId); + Future closeTab(String tabId, {String paymentMethod = 'cash'}) async { + await barTabService.closeTab(tabId, paymentMethod: paymentMethod); await _reloadTabs(); } diff --git a/lib/viewmodels/dev_menu_view_model.dart b/lib/viewmodels/dev_menu_view_model.dart index 4cb1af8..1df5e17 100644 --- a/lib/viewmodels/dev_menu_view_model.dart +++ b/lib/viewmodels/dev_menu_view_model.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:drift/drift.dart'; import 'package:kooltab2/database/app_database.dart'; import 'package:kooltab2/services/bar_tab_service.dart'; import 'package:kooltab2/services/pin_lock_service.dart'; @@ -274,6 +275,7 @@ class DevMenuViewModel extends ChangeNotifier { 'Sam', 'Tina', 'Umar', 'Vera', 'Wes', 'Xena', 'Yves', 'Zara', ]; + const paymentMethods = ['cash', 'payconiq']; await database.transaction(() async { for (var i = 0; i < count; i++) { @@ -295,6 +297,7 @@ class DevMenuViewModel extends ChangeNotifier { originalTabId: uuid.v4(), customerName: customer, closedAt: closedAt, + paymentMethod: Value(paymentMethods[(i * 3) % paymentMethods.length]), ), ); diff --git a/lib/views/dialogs/close_tab_dialog.dart b/lib/views/dialogs/close_tab_dialog.dart index c3c1302..a22c2d4 100644 --- a/lib/views/dialogs/close_tab_dialog.dart +++ b/lib/views/dialogs/close_tab_dialog.dart @@ -1,7 +1,9 @@ -import 'package:kooltab2/viewmodels/bar_screen_view_model.dart'; -import 'package:kooltab2/views/widgets/slide_confirm.dart'; -import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; + +import '../../viewmodels/bar_screen_view_model.dart'; +import '../widgets/slide_confirm.dart'; Future confirmCloseTab(BuildContext context) async { final viewModel = context.read(); @@ -9,39 +11,111 @@ Future confirmCloseTab(BuildContext context) async { if (tab == null) return; + String paymentMethod = 'cash'; + final confirmed = await showDialog( context: context, builder: (dialogContext) { - return AlertDialog( - title: Text('Close ${tab.customerName}ʼs tab?'), - content: SizedBox( - width: 360, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Current total: ${tab.formattedTotal}'), - const SizedBox(height: 20), - - SlideConfirm( - onConfirmed: () { - Navigator.of(dialogContext).pop(true); - }, + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: Text('Close ${tab.customerName}ʼs tab?'), + content: SizedBox( + width: 360, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Current total: ${tab.formattedTotal}'), + const SizedBox(height: 20), + Text( + 'Payment method', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 8), + _PaymentPicker( + selected: paymentMethod, + onChanged: (value) { + setDialogState(() => paymentMethod = value); + }, + ), + const SizedBox(height: 24), + SlideConfirm( + onConfirmed: () { + Navigator.of(dialogContext).pop(true); + }, + ), + ], + ), + ), + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: const Text('Cancel'), ), ], - ), - ), - actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(false), - child: const Text('Cancel'), - ), - ], + ); + }, ); }, ); if (confirmed != true) return; - await viewModel.closeSelectedTab(); + await viewModel.closeSelectedTab(paymentMethod: paymentMethod); } + +class _PaymentPicker extends StatelessWidget { + final String selected; + final ValueChanged onChanged; + + const _PaymentPicker({ + required this.selected, + required this.onChanged, + }); + + static const _options = [ + ('cash', 'Cash'), + ('payconiq', 'Payconiq'), + ]; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _options.map((option) { + final (value, label) = option; + final isSelected = selected == value; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: ChoiceChip( + showCheckmark: false, + selected: isSelected, + onSelected: (_) => onChanged(value), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (value == 'cash') + const Icon( + Icons.attach_money, + size: 16, + color: Colors.lightGreen, + ) + else + SvgPicture.asset( + 'assets/icons/payconic.svg', + width: 16, + height: 16, + colorFilter: ColorFilter.mode(Colors.pinkAccent, BlendMode.srcIn), + ), + const SizedBox(width: 6), + Text(label), + ], + ), + ), + ); + }).toList(), + ); + } +} \ No newline at end of file diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 09718d2..e7288ac 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -219,6 +219,7 @@ class _SettingsScreenViewState extends State { AppThemeMode.system => 'System', AppThemeMode.light => 'Light', AppThemeMode.dark => 'Dark', + AppThemeMode.ugly => 'Ugly', }, onTap: () async { final selected = await showModalBottomSheet( @@ -234,6 +235,7 @@ class _SettingsScreenViewState extends State { AppThemeMode.system => 'System', AppThemeMode.light => 'Light', AppThemeMode.dark => 'Dark', + AppThemeMode.ugly => 'Ugly', }), onChanged: (value) => Navigator.pop(context, value), diff --git a/lib/views/widgets/closed_tab_card.dart b/lib/views/widgets/closed_tab_card.dart index d9a31f8..4d4f583 100644 --- a/lib/views/widgets/closed_tab_card.dart +++ b/lib/views/widgets/closed_tab_card.dart @@ -15,6 +15,13 @@ class ClosedTabCard extends StatefulWidget { class _ClosedTabCardState extends State { bool _expanded = false; + IconData _paymentIcon(String method) { + return switch (method) { + 'payconiq' => Icons.qr_code_rounded, + _ => Icons.money_rounded, + }; + } + @override Widget build(BuildContext context) { final closedTab = widget.closedTab; @@ -49,15 +56,51 @@ class _ClosedTabCardState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - closedTab.customerName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: scheme.onSurface, - ), + Row( + children: [ + Expanded( + child: Text( + closedTab.customerName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: scheme.onSurface, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: scheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _paymentIcon(closedTab.paymentMethod), + size: 13, + color: scheme.primary, + ), + const SizedBox(width: 4), + Text( + closedTab.formattedPaymentMethod, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: scheme.primary, + ), + ), + ], + ), + ), + ], ), const SizedBox(height: 2), Text( @@ -163,4 +206,4 @@ class _ClosedTabItemRow extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index bae0bc8..c02a9a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -358,6 +358,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -680,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -728,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -925,6 +949,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "4dca4feb77dc3ec7f6e27e49c53241eb8217f55e4f9b12599a27f8903bca5682" + url: "https://pub.dev" + source: hosted + version: "1.3.0" vector_math: dependency: transitive description: @@ -989,6 +1037,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4e358cf..b45fa14 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: package_info_plus: ^10.1.0 open_filex: ^4.7.0 ota_update: ^7.1.0 + flutter_svg: ^2.3.0 # permission_handler: ^12.0.3 @@ -83,6 +84,9 @@ flutter: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg + assets: + - assets/icons/payconic.svg + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images