diff --git a/lib/app/app.dart b/lib/app/app.dart index 2c209d9..75c2752 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -14,10 +14,10 @@ class KoolTabApp extends StatelessWidget { routerConfig: appRouter, // theme: ThemeData( // useMaterial3: false, - // colorSchemeSeed: Colors.redAccent, - // brightness: Brightness.dark, + // colorSchemeSeed: Colors.blueAccent, + // brightness: Brightness.light, // ), - theme: neoBrutalDarkTheme + theme: darkTheme ); } } \ No newline at end of file diff --git a/lib/app/router.dart b/lib/app/router.dart index ab56c8c..e3d199a 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -1,16 +1,24 @@ +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../views/bar_screen_view.dart'; +import '../views/history_screen_view.dart'; import '../views/product_form_view.dart'; import '../views/product_list_view.dart'; +final RouteObserver routeObserver = RouteObserver(); + final appRouter = GoRouter( initialLocation: '/bar', + observers: [ + routeObserver, + ], routes: [ GoRoute( path: '/bar', builder: (context, state) => const BarScreenView(), ), + GoRoute( path: '/products', builder: (context, state) => const ProductListView(), @@ -29,5 +37,9 @@ final appRouter = GoRouter( ), ], ), + GoRoute( + path: '/history', + builder: (context, state) => const HistoryScreenView(), + ), ], ); \ No newline at end of file diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index b04ac35..da7b855 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -69,18 +69,45 @@ class TabItems extends Table { Set get primaryKey => {id}; } +@DataClassName('ClosedTabsRow') +class ClosedTabs extends Table { + TextColumn get id => text()(); + TextColumn get originalTabId => text()(); + TextColumn get customerName => text()(); + DateTimeColumn get closedAt => dateTime()(); + + @override + Set get primaryKey => {id}; +} + +@DataClassName('ClosedTabItemRow') +class ClosedTabItems extends Table { + TextColumn get id => text()(); + TextColumn get closedTabId => text()(); + TextColumn get productId => text()(); + TextColumn get productName => text()(); + IntColumn get quantity => integer()(); + IntColumn get unitPriceInCents => integer()(); + + @override + Set get primaryKey => {id}; +} + @DriftDatabase( tables: [ Products, BarTabs, TabItems, + + ClosedTabs, + ClosedTabItems ], ) class AppDatabase extends _$AppDatabase { AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration { @@ -97,6 +124,11 @@ class AppDatabase extends _$AppDatabase { if (from < 3) { await migrator.addColumn(products, products.imagePath); } + + if (from < 4){ + await migrator.createTable(closedTabs); + await migrator.createTable(closedTabItems); + } }, ); } diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 8d65e26..6fc833f 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -1369,12 +1369,771 @@ class TabItemsCompanion extends UpdateCompanion { } } +class $ClosedTabsTable extends ClosedTabs + with TableInfo<$ClosedTabsTable, ClosedTabsRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ClosedTabsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _originalTabIdMeta = const VerificationMeta( + 'originalTabId', + ); + @override + late final GeneratedColumn originalTabId = GeneratedColumn( + 'original_tab_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _customerNameMeta = const VerificationMeta( + 'customerName', + ); + @override + late final GeneratedColumn customerName = GeneratedColumn( + 'customer_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _closedAtMeta = const VerificationMeta( + 'closedAt', + ); + @override + late final GeneratedColumn closedAt = GeneratedColumn( + 'closed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + originalTabId, + customerName, + closedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'closed_tabs'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('original_tab_id')) { + context.handle( + _originalTabIdMeta, + originalTabId.isAcceptableOrUnknown( + data['original_tab_id']!, + _originalTabIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_originalTabIdMeta); + } + if (data.containsKey('customer_name')) { + context.handle( + _customerNameMeta, + customerName.isAcceptableOrUnknown( + data['customer_name']!, + _customerNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_customerNameMeta); + } + if (data.containsKey('closed_at')) { + context.handle( + _closedAtMeta, + closedAt.isAcceptableOrUnknown(data['closed_at']!, _closedAtMeta), + ); + } else if (isInserting) { + context.missing(_closedAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ClosedTabsRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ClosedTabsRow( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + originalTabId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}original_tab_id'], + )!, + customerName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_name'], + )!, + closedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}closed_at'], + )!, + ); + } + + @override + $ClosedTabsTable createAlias(String alias) { + return $ClosedTabsTable(attachedDatabase, alias); + } +} + +class ClosedTabsRow extends DataClass implements Insertable { + final String id; + final String originalTabId; + final String customerName; + final DateTime closedAt; + const ClosedTabsRow({ + required this.id, + required this.originalTabId, + required this.customerName, + required this.closedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['original_tab_id'] = Variable(originalTabId); + map['customer_name'] = Variable(customerName); + map['closed_at'] = Variable(closedAt); + return map; + } + + ClosedTabsCompanion toCompanion(bool nullToAbsent) { + return ClosedTabsCompanion( + id: Value(id), + originalTabId: Value(originalTabId), + customerName: Value(customerName), + closedAt: Value(closedAt), + ); + } + + factory ClosedTabsRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ClosedTabsRow( + id: serializer.fromJson(json['id']), + originalTabId: serializer.fromJson(json['originalTabId']), + customerName: serializer.fromJson(json['customerName']), + closedAt: serializer.fromJson(json['closedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'originalTabId': serializer.toJson(originalTabId), + 'customerName': serializer.toJson(customerName), + 'closedAt': serializer.toJson(closedAt), + }; + } + + ClosedTabsRow copyWith({ + String? id, + String? originalTabId, + String? customerName, + DateTime? closedAt, + }) => ClosedTabsRow( + id: id ?? this.id, + originalTabId: originalTabId ?? this.originalTabId, + customerName: customerName ?? this.customerName, + closedAt: closedAt ?? this.closedAt, + ); + ClosedTabsRow copyWithCompanion(ClosedTabsCompanion data) { + return ClosedTabsRow( + id: data.id.present ? data.id.value : this.id, + originalTabId: data.originalTabId.present + ? data.originalTabId.value + : this.originalTabId, + customerName: data.customerName.present + ? data.customerName.value + : this.customerName, + closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ClosedTabsRow(') + ..write('id: $id, ') + ..write('originalTabId: $originalTabId, ') + ..write('customerName: $customerName, ') + ..write('closedAt: $closedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, originalTabId, customerName, closedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ClosedTabsRow && + other.id == this.id && + other.originalTabId == this.originalTabId && + other.customerName == this.customerName && + other.closedAt == this.closedAt); +} + +class ClosedTabsCompanion extends UpdateCompanion { + final Value id; + final Value originalTabId; + final Value customerName; + final Value closedAt; + 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.rowid = const Value.absent(), + }); + ClosedTabsCompanion.insert({ + required String id, + required String originalTabId, + required String customerName, + required DateTime closedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + originalTabId = Value(originalTabId), + customerName = Value(customerName), + closedAt = Value(closedAt); + static Insertable custom({ + Expression? id, + Expression? originalTabId, + Expression? customerName, + Expression? closedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (originalTabId != null) 'original_tab_id': originalTabId, + if (customerName != null) 'customer_name': customerName, + if (closedAt != null) 'closed_at': closedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ClosedTabsCompanion copyWith({ + Value? id, + Value? originalTabId, + Value? customerName, + Value? closedAt, + Value? rowid, + }) { + return ClosedTabsCompanion( + id: id ?? this.id, + originalTabId: originalTabId ?? this.originalTabId, + customerName: customerName ?? this.customerName, + closedAt: closedAt ?? this.closedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (originalTabId.present) { + map['original_tab_id'] = Variable(originalTabId.value); + } + if (customerName.present) { + map['customer_name'] = Variable(customerName.value); + } + if (closedAt.present) { + map['closed_at'] = Variable(closedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ClosedTabsCompanion(') + ..write('id: $id, ') + ..write('originalTabId: $originalTabId, ') + ..write('customerName: $customerName, ') + ..write('closedAt: $closedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ClosedTabItemsTable extends ClosedTabItems + with TableInfo<$ClosedTabItemsTable, ClosedTabItemRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ClosedTabItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _closedTabIdMeta = const VerificationMeta( + 'closedTabId', + ); + @override + late final GeneratedColumn closedTabId = GeneratedColumn( + 'closed_tab_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _productIdMeta = const VerificationMeta( + 'productId', + ); + @override + late final GeneratedColumn productId = GeneratedColumn( + 'product_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _productNameMeta = const VerificationMeta( + 'productName', + ); + @override + late final GeneratedColumn productName = GeneratedColumn( + 'product_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _quantityMeta = const VerificationMeta( + 'quantity', + ); + @override + late final GeneratedColumn quantity = GeneratedColumn( + 'quantity', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _unitPriceInCentsMeta = const VerificationMeta( + 'unitPriceInCents', + ); + @override + late final GeneratedColumn unitPriceInCents = GeneratedColumn( + 'unit_price_in_cents', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + closedTabId, + productId, + productName, + quantity, + unitPriceInCents, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'closed_tab_items'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('closed_tab_id')) { + context.handle( + _closedTabIdMeta, + closedTabId.isAcceptableOrUnknown( + data['closed_tab_id']!, + _closedTabIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_closedTabIdMeta); + } + if (data.containsKey('product_id')) { + context.handle( + _productIdMeta, + productId.isAcceptableOrUnknown(data['product_id']!, _productIdMeta), + ); + } else if (isInserting) { + context.missing(_productIdMeta); + } + if (data.containsKey('product_name')) { + context.handle( + _productNameMeta, + productName.isAcceptableOrUnknown( + data['product_name']!, + _productNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_productNameMeta); + } + if (data.containsKey('quantity')) { + context.handle( + _quantityMeta, + quantity.isAcceptableOrUnknown(data['quantity']!, _quantityMeta), + ); + } else if (isInserting) { + context.missing(_quantityMeta); + } + if (data.containsKey('unit_price_in_cents')) { + context.handle( + _unitPriceInCentsMeta, + unitPriceInCents.isAcceptableOrUnknown( + data['unit_price_in_cents']!, + _unitPriceInCentsMeta, + ), + ); + } else if (isInserting) { + context.missing(_unitPriceInCentsMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ClosedTabItemRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ClosedTabItemRow( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + closedTabId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}closed_tab_id'], + )!, + productId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_id'], + )!, + productName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_name'], + )!, + quantity: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quantity'], + )!, + unitPriceInCents: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}unit_price_in_cents'], + )!, + ); + } + + @override + $ClosedTabItemsTable createAlias(String alias) { + return $ClosedTabItemsTable(attachedDatabase, alias); + } +} + +class ClosedTabItemRow extends DataClass + implements Insertable { + final String id; + final String closedTabId; + final String productId; + final String productName; + final int quantity; + final int unitPriceInCents; + const ClosedTabItemRow({ + required this.id, + required this.closedTabId, + required this.productId, + required this.productName, + required this.quantity, + required this.unitPriceInCents, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['closed_tab_id'] = Variable(closedTabId); + map['product_id'] = Variable(productId); + map['product_name'] = Variable(productName); + map['quantity'] = Variable(quantity); + map['unit_price_in_cents'] = Variable(unitPriceInCents); + return map; + } + + ClosedTabItemsCompanion toCompanion(bool nullToAbsent) { + return ClosedTabItemsCompanion( + id: Value(id), + closedTabId: Value(closedTabId), + productId: Value(productId), + productName: Value(productName), + quantity: Value(quantity), + unitPriceInCents: Value(unitPriceInCents), + ); + } + + factory ClosedTabItemRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ClosedTabItemRow( + id: serializer.fromJson(json['id']), + closedTabId: serializer.fromJson(json['closedTabId']), + productId: serializer.fromJson(json['productId']), + productName: serializer.fromJson(json['productName']), + quantity: serializer.fromJson(json['quantity']), + unitPriceInCents: serializer.fromJson(json['unitPriceInCents']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'closedTabId': serializer.toJson(closedTabId), + 'productId': serializer.toJson(productId), + 'productName': serializer.toJson(productName), + 'quantity': serializer.toJson(quantity), + 'unitPriceInCents': serializer.toJson(unitPriceInCents), + }; + } + + ClosedTabItemRow copyWith({ + String? id, + String? closedTabId, + String? productId, + String? productName, + int? quantity, + int? unitPriceInCents, + }) => ClosedTabItemRow( + id: id ?? this.id, + closedTabId: closedTabId ?? this.closedTabId, + productId: productId ?? this.productId, + productName: productName ?? this.productName, + quantity: quantity ?? this.quantity, + unitPriceInCents: unitPriceInCents ?? this.unitPriceInCents, + ); + ClosedTabItemRow copyWithCompanion(ClosedTabItemsCompanion data) { + return ClosedTabItemRow( + id: data.id.present ? data.id.value : this.id, + closedTabId: data.closedTabId.present + ? data.closedTabId.value + : this.closedTabId, + productId: data.productId.present ? data.productId.value : this.productId, + productName: data.productName.present + ? data.productName.value + : this.productName, + quantity: data.quantity.present ? data.quantity.value : this.quantity, + unitPriceInCents: data.unitPriceInCents.present + ? data.unitPriceInCents.value + : this.unitPriceInCents, + ); + } + + @override + String toString() { + return (StringBuffer('ClosedTabItemRow(') + ..write('id: $id, ') + ..write('closedTabId: $closedTabId, ') + ..write('productId: $productId, ') + ..write('productName: $productName, ') + ..write('quantity: $quantity, ') + ..write('unitPriceInCents: $unitPriceInCents') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + closedTabId, + productId, + productName, + quantity, + unitPriceInCents, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ClosedTabItemRow && + other.id == this.id && + other.closedTabId == this.closedTabId && + other.productId == this.productId && + other.productName == this.productName && + other.quantity == this.quantity && + other.unitPriceInCents == this.unitPriceInCents); +} + +class ClosedTabItemsCompanion extends UpdateCompanion { + final Value id; + final Value closedTabId; + final Value productId; + final Value productName; + final Value quantity; + final Value unitPriceInCents; + final Value rowid; + const ClosedTabItemsCompanion({ + this.id = const Value.absent(), + this.closedTabId = const Value.absent(), + this.productId = const Value.absent(), + this.productName = const Value.absent(), + this.quantity = const Value.absent(), + this.unitPriceInCents = const Value.absent(), + this.rowid = const Value.absent(), + }); + ClosedTabItemsCompanion.insert({ + required String id, + required String closedTabId, + required String productId, + required String productName, + required int quantity, + required int unitPriceInCents, + this.rowid = const Value.absent(), + }) : id = Value(id), + closedTabId = Value(closedTabId), + productId = Value(productId), + productName = Value(productName), + quantity = Value(quantity), + unitPriceInCents = Value(unitPriceInCents); + static Insertable custom({ + Expression? id, + Expression? closedTabId, + Expression? productId, + Expression? productName, + Expression? quantity, + Expression? unitPriceInCents, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (closedTabId != null) 'closed_tab_id': closedTabId, + if (productId != null) 'product_id': productId, + if (productName != null) 'product_name': productName, + if (quantity != null) 'quantity': quantity, + if (unitPriceInCents != null) 'unit_price_in_cents': unitPriceInCents, + if (rowid != null) 'rowid': rowid, + }); + } + + ClosedTabItemsCompanion copyWith({ + Value? id, + Value? closedTabId, + Value? productId, + Value? productName, + Value? quantity, + Value? unitPriceInCents, + Value? rowid, + }) { + return ClosedTabItemsCompanion( + id: id ?? this.id, + closedTabId: closedTabId ?? this.closedTabId, + productId: productId ?? this.productId, + productName: productName ?? this.productName, + quantity: quantity ?? this.quantity, + unitPriceInCents: unitPriceInCents ?? this.unitPriceInCents, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (closedTabId.present) { + map['closed_tab_id'] = Variable(closedTabId.value); + } + if (productId.present) { + map['product_id'] = Variable(productId.value); + } + if (productName.present) { + map['product_name'] = Variable(productName.value); + } + if (quantity.present) { + map['quantity'] = Variable(quantity.value); + } + if (unitPriceInCents.present) { + map['unit_price_in_cents'] = Variable(unitPriceInCents.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ClosedTabItemsCompanion(') + ..write('id: $id, ') + ..write('closedTabId: $closedTabId, ') + ..write('productId: $productId, ') + ..write('productName: $productName, ') + ..write('quantity: $quantity, ') + ..write('unitPriceInCents: $unitPriceInCents, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); late final $ProductsTable products = $ProductsTable(this); late final $BarTabsTable barTabs = $BarTabsTable(this); late final $TabItemsTable tabItems = $TabItemsTable(this); + late final $ClosedTabsTable closedTabs = $ClosedTabsTable(this); + late final $ClosedTabItemsTable closedTabItems = $ClosedTabItemsTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -1383,6 +2142,8 @@ abstract class _$AppDatabase extends GeneratedDatabase { products, barTabs, tabItems, + closedTabs, + closedTabItems, ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ @@ -2500,6 +3261,422 @@ typedef $$TabItemsTableProcessedTableManager = TabItemRow, PrefetchHooks Function({bool tabId, bool productId}) >; +typedef $$ClosedTabsTableCreateCompanionBuilder = + ClosedTabsCompanion Function({ + required String id, + required String originalTabId, + required String customerName, + required DateTime closedAt, + Value rowid, + }); +typedef $$ClosedTabsTableUpdateCompanionBuilder = + ClosedTabsCompanion Function({ + Value id, + Value originalTabId, + Value customerName, + Value closedAt, + Value rowid, + }); + +class $$ClosedTabsTableFilterComposer + extends Composer<_$AppDatabase, $ClosedTabsTable> { + $$ClosedTabsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get originalTabId => $composableBuilder( + column: $table.originalTabId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get customerName => $composableBuilder( + column: $table.customerName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get closedAt => $composableBuilder( + column: $table.closedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ClosedTabsTableOrderingComposer + extends Composer<_$AppDatabase, $ClosedTabsTable> { + $$ClosedTabsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get originalTabId => $composableBuilder( + column: $table.originalTabId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get customerName => $composableBuilder( + column: $table.customerName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get closedAt => $composableBuilder( + column: $table.closedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ClosedTabsTableAnnotationComposer + extends Composer<_$AppDatabase, $ClosedTabsTable> { + $$ClosedTabsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get originalTabId => $composableBuilder( + column: $table.originalTabId, + builder: (column) => column, + ); + + GeneratedColumn get customerName => $composableBuilder( + column: $table.customerName, + builder: (column) => column, + ); + + GeneratedColumn get closedAt => + $composableBuilder(column: $table.closedAt, builder: (column) => column); +} + +class $$ClosedTabsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ClosedTabsTable, + ClosedTabsRow, + $$ClosedTabsTableFilterComposer, + $$ClosedTabsTableOrderingComposer, + $$ClosedTabsTableAnnotationComposer, + $$ClosedTabsTableCreateCompanionBuilder, + $$ClosedTabsTableUpdateCompanionBuilder, + ( + ClosedTabsRow, + BaseReferences<_$AppDatabase, $ClosedTabsTable, ClosedTabsRow>, + ), + ClosedTabsRow, + PrefetchHooks Function() + > { + $$ClosedTabsTableTableManager(_$AppDatabase db, $ClosedTabsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ClosedTabsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ClosedTabsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ClosedTabsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value originalTabId = const Value.absent(), + Value customerName = const Value.absent(), + Value closedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ClosedTabsCompanion( + id: id, + originalTabId: originalTabId, + customerName: customerName, + closedAt: closedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String originalTabId, + required String customerName, + required DateTime closedAt, + Value rowid = const Value.absent(), + }) => ClosedTabsCompanion.insert( + id: id, + originalTabId: originalTabId, + customerName: customerName, + closedAt: closedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ClosedTabsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ClosedTabsTable, + ClosedTabsRow, + $$ClosedTabsTableFilterComposer, + $$ClosedTabsTableOrderingComposer, + $$ClosedTabsTableAnnotationComposer, + $$ClosedTabsTableCreateCompanionBuilder, + $$ClosedTabsTableUpdateCompanionBuilder, + ( + ClosedTabsRow, + BaseReferences<_$AppDatabase, $ClosedTabsTable, ClosedTabsRow>, + ), + ClosedTabsRow, + PrefetchHooks Function() + >; +typedef $$ClosedTabItemsTableCreateCompanionBuilder = + ClosedTabItemsCompanion Function({ + required String id, + required String closedTabId, + required String productId, + required String productName, + required int quantity, + required int unitPriceInCents, + Value rowid, + }); +typedef $$ClosedTabItemsTableUpdateCompanionBuilder = + ClosedTabItemsCompanion Function({ + Value id, + Value closedTabId, + Value productId, + Value productName, + Value quantity, + Value unitPriceInCents, + Value rowid, + }); + +class $$ClosedTabItemsTableFilterComposer + extends Composer<_$AppDatabase, $ClosedTabItemsTable> { + $$ClosedTabItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get closedTabId => $composableBuilder( + column: $table.closedTabId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get productId => $composableBuilder( + column: $table.productId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get productName => $composableBuilder( + column: $table.productName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get unitPriceInCents => $composableBuilder( + column: $table.unitPriceInCents, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ClosedTabItemsTableOrderingComposer + extends Composer<_$AppDatabase, $ClosedTabItemsTable> { + $$ClosedTabItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get closedTabId => $composableBuilder( + column: $table.closedTabId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get productId => $composableBuilder( + column: $table.productId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get productName => $composableBuilder( + column: $table.productName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get unitPriceInCents => $composableBuilder( + column: $table.unitPriceInCents, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ClosedTabItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $ClosedTabItemsTable> { + $$ClosedTabItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get closedTabId => $composableBuilder( + column: $table.closedTabId, + builder: (column) => column, + ); + + GeneratedColumn get productId => + $composableBuilder(column: $table.productId, builder: (column) => column); + + GeneratedColumn get productName => $composableBuilder( + column: $table.productName, + builder: (column) => column, + ); + + GeneratedColumn get quantity => + $composableBuilder(column: $table.quantity, builder: (column) => column); + + GeneratedColumn get unitPriceInCents => $composableBuilder( + column: $table.unitPriceInCents, + builder: (column) => column, + ); +} + +class $$ClosedTabItemsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ClosedTabItemsTable, + ClosedTabItemRow, + $$ClosedTabItemsTableFilterComposer, + $$ClosedTabItemsTableOrderingComposer, + $$ClosedTabItemsTableAnnotationComposer, + $$ClosedTabItemsTableCreateCompanionBuilder, + $$ClosedTabItemsTableUpdateCompanionBuilder, + ( + ClosedTabItemRow, + BaseReferences< + _$AppDatabase, + $ClosedTabItemsTable, + ClosedTabItemRow + >, + ), + ClosedTabItemRow, + PrefetchHooks Function() + > { + $$ClosedTabItemsTableTableManager( + _$AppDatabase db, + $ClosedTabItemsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ClosedTabItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ClosedTabItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ClosedTabItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value closedTabId = const Value.absent(), + Value productId = const Value.absent(), + Value productName = const Value.absent(), + Value quantity = const Value.absent(), + Value unitPriceInCents = const Value.absent(), + Value rowid = const Value.absent(), + }) => ClosedTabItemsCompanion( + id: id, + closedTabId: closedTabId, + productId: productId, + productName: productName, + quantity: quantity, + unitPriceInCents: unitPriceInCents, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String closedTabId, + required String productId, + required String productName, + required int quantity, + required int unitPriceInCents, + Value rowid = const Value.absent(), + }) => ClosedTabItemsCompanion.insert( + id: id, + closedTabId: closedTabId, + productId: productId, + productName: productName, + quantity: quantity, + unitPriceInCents: unitPriceInCents, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ClosedTabItemsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ClosedTabItemsTable, + ClosedTabItemRow, + $$ClosedTabItemsTableFilterComposer, + $$ClosedTabItemsTableOrderingComposer, + $$ClosedTabItemsTableAnnotationComposer, + $$ClosedTabItemsTableCreateCompanionBuilder, + $$ClosedTabItemsTableUpdateCompanionBuilder, + ( + ClosedTabItemRow, + BaseReferences<_$AppDatabase, $ClosedTabItemsTable, ClosedTabItemRow>, + ), + ClosedTabItemRow, + PrefetchHooks Function() + >; class $AppDatabaseManager { final _$AppDatabase _db; @@ -2510,4 +3687,8 @@ class $AppDatabaseManager { $$BarTabsTableTableManager(_db, _db.barTabs); $$TabItemsTableTableManager get tabItems => $$TabItemsTableTableManager(_db, _db.tabItems); + $$ClosedTabsTableTableManager get closedTabs => + $$ClosedTabsTableTableManager(_db, _db.closedTabs); + $$ClosedTabItemsTableTableManager get closedTabItems => + $$ClosedTabItemsTableTableManager(_db, _db.closedTabItems); } diff --git a/lib/main.dart b/lib/main.dart index 6bd7147..e4b8458 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:kooltab2/viewmodels/history_view_model.dart'; import 'package:provider/provider.dart'; import 'app/app.dart'; @@ -13,6 +16,11 @@ import 'viewmodels/product_list_view_model.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + + await initializeDateFormatting('nl_BE', null); + Intl.defaultLocale = 'nl_BE'; + + await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, @@ -45,6 +53,9 @@ Future main() async { barTabService: context.read(), ), ), + ChangeNotifierProvider( + create: (context) => HistoryViewModel(barTabService: context.read()), + ) ], child: const AppBootstrap( child: KoolTabApp(), diff --git a/lib/models/closed_tab.dart b/lib/models/closed_tab.dart new file mode 100644 index 0000000..1b8c405 --- /dev/null +++ b/lib/models/closed_tab.dart @@ -0,0 +1,27 @@ +import 'package:intl/intl.dart'; + +import 'closed_tab_item.dart'; + +class ClosedTab { + final String id; + final String originalTabId; + final String customerName; + final DateTime closedAt; + final List items; + + ClosedTab({ + required this.id, + required this.originalTabId, + required this.customerName, + required this.closedAt, + required this.items, + }); + + int get itemCount => items.fold(0, (sum, item) => sum + item.quantity); + + int get totalInCents => + items.fold(0, (sum, item) => sum + item.quantity * item.unitPriceInCents); + + String get formattedTotal => + NumberFormat.simpleCurrency().format(totalInCents / 100); +} \ No newline at end of file diff --git a/lib/models/closed_tab_item.dart b/lib/models/closed_tab_item.dart new file mode 100644 index 0000000..18fe7fb --- /dev/null +++ b/lib/models/closed_tab_item.dart @@ -0,0 +1,19 @@ +class ClosedTabItem { + final String id; + final String closedTabId; + final String productId; + final String productName; + final int quantity; + final int unitPriceInCents; + + ClosedTabItem({ + required this.id, + required this.closedTabId, + required this.productId, + required this.productName, + required this.quantity, + required this.unitPriceInCents, + }); + + int get lineTotalInCents => quantity * unitPriceInCents; +} \ No newline at end of file diff --git a/lib/services/bar_tab_service.dart b/lib/services/bar_tab_service.dart index e8d8e94..2db5b7a 100644 --- a/lib/services/bar_tab_service.dart +++ b/lib/services/bar_tab_service.dart @@ -3,6 +3,8 @@ import 'package:uuid/uuid.dart'; import '../database/app_database.dart'; import '../models/bar_tab.dart'; +import '../models/closed_tab.dart'; +import '../models/closed_tab_item.dart'; import '../models/product.dart'; import '../models/tab_item.dart'; @@ -25,7 +27,11 @@ abstract class BarTabService { required int quantity, }); + /// 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> getClosedTabs(); } class DriftBarTabService implements BarTabService { @@ -61,6 +67,17 @@ class DriftBarTabService implements BarTabService { ); } + ClosedTabItem _mapClosedItemRow(ClosedTabItemRow row) { + return ClosedTabItem( + id: row.id, + closedTabId: row.closedTabId, + productId: row.productId, + productName: row.productName, + quantity: row.quantity, + unitPriceInCents: row.unitPriceInCents, + ); + } + Future> _getItemsForTab(String tabId) async { final query = database.select(database.tabItems) ..where((item) => item.tabId.equals(tabId)) @@ -201,14 +218,83 @@ class DriftBarTabService implements BarTabService { @override Future closeTab(String tabId) async { - final updateQuery = database.update(database.barTabs) - ..where((tab) => tab.id.equals(tabId)); + await database.transaction(() async { + final tabQuery = database.select(database.barTabs) + ..where((tab) => tab.id.equals(tabId)); - await updateQuery.write( - BarTabsCompanion( - status: const Value('closed'), - closedAt: Value(DateTime.now()), - ), - ); + final tabRow = await tabQuery.getSingleOrNull(); + + if (tabRow == null) { + return; + } + + final items = await _getItemsForTab(tabId); + + if (items.isEmpty) { + // Nothing to settle, leave the tab as-is. + return; + } + + final closedTabId = _uuid.v4(); + + await database.into(database.closedTabs).insert( + ClosedTabsCompanion.insert( + id: closedTabId, + originalTabId: tabId, + customerName: tabRow.customerName, + closedAt: DateTime.now(), + ), + ); + + for (final item in items) { + await database.into(database.closedTabItems).insert( + ClosedTabItemsCompanion.insert( + id: _uuid.v4(), + closedTabId: closedTabId, + productId: item.productId, + productName: item.productName, + quantity: item.quantity, + unitPriceInCents: item.unitPriceInCents, + ), + ); + } + + final deleteQuery = database.delete(database.tabItems) + ..where((item) => item.tabId.equals(tabId)); + + await deleteQuery.go(); + + // Note: tab status/customerName untouched on purpose — the tab + // stays open so it keeps showing in the open tabs list. + }); + } + + @override + Future> getClosedTabs() async { + final query = database.select(database.closedTabs) + ..orderBy([ + (tab) => OrderingTerm.desc(tab.closedAt), + ]); + + final closedTabRows = await query.get(); + + final closedTabs = []; + + for (final row in closedTabRows) { + final itemsQuery = database.select(database.closedTabItems) + ..where((item) => item.closedTabId.equals(row.id)); + + final itemRows = await itemsQuery.get(); + + closedTabs.add(ClosedTab( + id: row.id, + originalTabId: row.originalTabId, + customerName: row.customerName, + closedAt: row.closedAt, + items: itemRows.map(_mapClosedItemRow).toList(), + )); + } + + return closedTabs; } } \ No newline at end of file diff --git a/lib/theme.dart b/lib/theme.dart index 3b8cb22..e0a8a58 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -1,38 +1,108 @@ import 'package:flutter/material.dart'; +// --------------------------------------------------------------------------- +// Shared design tokens +// --------------------------------------------------------------------------- + +class AppRadii { + static const double sm = 10; + static const double md = 14; + static const double lg = 20; + static const double xl = 28; +} + +class AppSpacing { + static const double xs = 4; + static const double sm = 8; + static const double md = 12; + static const double lg = 16; + static const double xl = 24; +} + +// --------------------------------------------------------------------------- +// Dark theme — refined violet palette +// --------------------------------------------------------------------------- + +const _violet = Color(0xFF8B5CF6); +const _violetSoft = Color(0xFFB39DFF); +const _bg = Color(0xFF111014); +const _surface = Color(0xFF1A1920); +const _surfaceRaised = Color(0xFF221F29); +const _outline = Color(0x1FFFFFFF); // ~12% white + final ThemeData darkTheme = ThemeData( useMaterial3: true, brightness: Brightness.dark, + splashFactory: InkRipple.splashFactory, colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF7C4DFF), + seedColor: _violet, brightness: Brightness.dark, + surface: _surface, + error: const Color(0xFFFF6B6B), ), - scaffoldBackgroundColor: const Color(0xFF121212), - canvasColor: const Color(0xFF121212), + scaffoldBackgroundColor: _bg, + canvasColor: _bg, - appBarTheme: const AppBarTheme( + appBarTheme: AppBarTheme( elevation: 0, - centerTitle: true, - backgroundColor: Colors.transparent, + centerTitle: false, + backgroundColor: _bg, foregroundColor: Colors.white, surfaceTintColor: Colors.transparent, - titleTextStyle: TextStyle( + titleTextStyle: const TextStyle( fontSize: 22, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, color: Colors.white, ), + iconTheme: const IconThemeData(color: Colors.white70, size: 22), + actionsIconTheme: const IconThemeData(color: Colors.white70, size: 22), + ), + + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + backgroundColor: Colors.white.withOpacity(0.05), + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + padding: const EdgeInsets.all(10), + ), ), cardTheme: CardThemeData( - color: const Color(0xFF1E1E1E), + color: _surface, elevation: 0, - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + margin: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide( - color: Colors.white.withOpacity(0.06), + borderRadius: BorderRadius.circular(AppRadii.lg), + side: const BorderSide(color: _outline, width: 1), + ), + ), + + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + elevation: 0, + backgroundColor: _violet, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.white.withOpacity(0.06), + disabledForegroundColor: Colors.white24, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + letterSpacing: 0.1, ), ), ), @@ -40,175 +110,214 @@ final ThemeData darkTheme = ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( elevation: 0, - backgroundColor: const Color(0xFF7C4DFF), + backgroundColor: _violet, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - textStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + borderRadius: BorderRadius.circular(AppRadii.md), ), + textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: Colors.white, - side: BorderSide( - color: Colors.white.withOpacity(0.15), - ), + side: const BorderSide(color: _outline, width: 1.4), padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadii.md), + ), + ), + ), + + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.white70, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), ), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: const Color(0xFF242424), + fillColor: _surfaceRaised, + hintStyle: const TextStyle(color: Colors.white38), contentPadding: const EdgeInsets.symmetric( - horizontal: 18, - vertical: 16, + horizontal: AppSpacing.lg, + vertical: AppSpacing.lg, ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadii.md), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide( - color: Colors.white.withOpacity(0.08), - ), + borderRadius: BorderRadius.circular(AppRadii.md), + borderSide: const BorderSide(color: _outline), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Color(0xFF7C4DFF), - width: 2, - ), + borderRadius: BorderRadius.circular(AppRadii.md), + borderSide: const BorderSide(color: _violet, width: 2), ), ), + dialogTheme: DialogThemeData( + backgroundColor: _surfaceRaised, + surfaceTintColor: Colors.transparent, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + titleTextStyle: const TextStyle( + color: Colors.white, + fontSize: 19, + fontWeight: FontWeight.w800, + ), + contentTextStyle: const TextStyle(color: Colors.white70, height: 1.4), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( - backgroundColor: Color(0xFF7C4DFF), + backgroundColor: _violet, foregroundColor: Colors.white, elevation: 2, ), navigationBarTheme: NavigationBarThemeData( - backgroundColor: const Color(0xFF1A1A1A), - indicatorColor: const Color(0xFF7C4DFF).withOpacity(0.25), - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontWeight: FontWeight.w600, - ), + backgroundColor: _surface, + indicatorColor: _violet.withOpacity(0.25), + labelTextStyle: const WidgetStatePropertyAll( + TextStyle(fontWeight: FontWeight.w600), ), ), listTileTheme: ListTileThemeData( - tileColor: const Color(0xFF1E1E1E), + tileColor: Colors.transparent, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadii.md), ), - iconColor: const Color(0xFFB39DFF), + iconColor: _violetSoft, textColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), ), snackBarTheme: SnackBarThemeData( - backgroundColor: const Color(0xFF2A2A2A), + backgroundColor: _surfaceRaised, behavior: SnackBarBehavior.floating, + elevation: 4, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadii.md), + side: const BorderSide(color: _outline), ), + contentTextStyle: const TextStyle(color: Colors.white), ), - dividerTheme: DividerThemeData( - color: Colors.white.withOpacity(0.08), - thickness: 1, + dividerTheme: const DividerThemeData(color: _outline, thickness: 1, space: 1), + + chipTheme: ChipThemeData( + backgroundColor: _surfaceRaised, + labelStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.sm), + side: const BorderSide(color: _outline), + ), ), textTheme: const TextTheme( - displayLarge: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + displayLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w800), headlineMedium: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, ), titleLarge: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + letterSpacing: -0.2, + ), + titleMedium: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, ), - bodyLarge: TextStyle( - color: Colors.white70, - fontSize: 16, - height: 1.5, - ), - bodyMedium: TextStyle( - color: Colors.white60, - height: 1.4, - ), - labelLarge: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - ), + bodyLarge: TextStyle(color: Colors.white70, fontSize: 16, height: 1.5), + bodyMedium: TextStyle(color: Colors.white60, height: 1.4), + bodySmall: TextStyle(color: Colors.white38, height: 1.3), + labelLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), ), ); +// --------------------------------------------------------------------------- +// Neo-brutalist dark theme — punchy, high-contrast alternative +// --------------------------------------------------------------------------- + +const _nbBg = Color(0xFF0C0C0E); +const _nbSurface = Color(0xFF1C1C20); +const _nbYellow = Color(0xFFFFD60A); +const _nbCyan = Color(0xFF00E5FF); +const _nbPink = Color(0xFFFF3D81); + final ThemeData neoBrutalDarkTheme = ThemeData( useMaterial3: true, brightness: Brightness.dark, colorScheme: const ColorScheme.dark( - primary: Color(0xFFFFD60A), // Bright yellow - secondary: Color(0xFF00E5FF), // Cyan - surface: Color(0xFF1A1A1A), - error: Color(0xFFFF5252), + primary: _nbYellow, + secondary: _nbCyan, + surface: _nbSurface, + error: _nbPink, ), - scaffoldBackgroundColor: const Color(0xFF0E0E0E), - canvasColor: const Color(0xFF0E0E0E), + scaffoldBackgroundColor: _nbBg, + canvasColor: _nbBg, textTheme: const TextTheme( displayLarge: TextStyle( fontSize: 48, fontWeight: FontWeight.w900, color: Colors.white, - letterSpacing: -1, + letterSpacing: -1.5, ), headlineMedium: TextStyle( fontSize: 32, fontWeight: FontWeight.w900, color: Colors.white, + letterSpacing: -0.5, ), titleLarge: TextStyle( fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.white, + ), + titleMedium: TextStyle( + fontSize: 17, fontWeight: FontWeight.w800, color: Colors.white, ), - bodyLarge: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - bodyMedium: TextStyle( - fontSize: 14, - color: Colors.white70, - ), + bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + bodyMedium: TextStyle(fontSize: 14, color: Colors.white70), + bodySmall: TextStyle(fontSize: 12, color: Colors.white54, fontWeight: FontWeight.w600), ), appBarTheme: const AppBarTheme( - backgroundColor: Color(0xFF0E0E0E), + backgroundColor: _nbBg, foregroundColor: Colors.white, elevation: 0, centerTitle: false, @@ -220,108 +329,357 @@ final ThemeData neoBrutalDarkTheme = ThemeData( ), cardTheme: CardThemeData( - color: const Color(0xFF242424), + color: _nbSurface, + elevation: 0, + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.white, width: 3), + ), + ), + + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: _nbYellow, + foregroundColor: Colors.black, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.black, width: 3), + ), + textStyle: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), + ), + + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: _nbYellow, + foregroundColor: Colors.black, + elevation: 0, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.black, width: 3), + ), + textStyle: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white, width: 3), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + textStyle: const TextStyle(fontWeight: FontWeight.w800), + ), + ), + + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: _nbSurface, + hintStyle: const TextStyle(color: Colors.white38, fontWeight: FontWeight.w600), + contentPadding: const EdgeInsets.all(18), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.white, width: 3), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.white, width: 3), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: _nbYellow, width: 4), + ), + ), + + dialogTheme: DialogThemeData( + backgroundColor: _nbSurface, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: Colors.white, - width: 3, + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.white, width: 3), + ), + titleTextStyle: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w900, + ), + contentTextStyle: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w600), + ), + + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: _nbPink, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + side: BorderSide(color: Colors.white, width: 3), + ), + ), + + snackBarTheme: SnackBarThemeData( + backgroundColor: _nbSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.white, width: 3), + ), + behavior: SnackBarBehavior.floating, + contentTextStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600), + ), + + dividerTheme: const DividerThemeData(color: Colors.white, thickness: 3), + + listTileTheme: ListTileThemeData( + tileColor: _nbSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.white, width: 3), + ), + textColor: Colors.white, + iconColor: _nbYellow, + ), +); + +const _violetL = Color(0xFF7C3AED); +const _violetSoftL = Color(0xFF6D28D9); +const _bgL = Color(0xFFFAFAFC); +const _surfaceL = Color(0xFFFFFFFF); +const _surfaceRaisedL = Color(0xFFF3F1F8); +const _outlineL = Color(0x14000000); // ~8% black + +final ThemeData lightTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.light, + splashFactory: InkRipple.splashFactory, + + colorScheme: ColorScheme.fromSeed( + seedColor: _violetL, + brightness: Brightness.light, + surface: _surfaceL, + error: const Color(0xFFE5484D), + ), + + scaffoldBackgroundColor: _bgL, + canvasColor: _bgL, + + appBarTheme: AppBarTheme( + elevation: 0, + centerTitle: false, + backgroundColor: _bgL, + foregroundColor: Colors.black87, + surfaceTintColor: Colors.transparent, + titleTextStyle: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, + color: Colors.black87, + ), + iconTheme: const IconThemeData(color: Colors.black54, size: 22), + actionsIconTheme: const IconThemeData(color: Colors.black54, size: 22), + ), + + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.04), + foregroundColor: Colors.black54, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + padding: const EdgeInsets.all(10), + ), + ), + + cardTheme: CardThemeData( + color: _surfaceL, + elevation: 0, + margin: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + side: const BorderSide(color: _outlineL, width: 1), + ), + ), + + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + elevation: 0, + backgroundColor: _violetL, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.black.withOpacity(0.06), + disabledForegroundColor: Colors.black26, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + letterSpacing: 0.1, ), ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFFFD60A), - foregroundColor: Colors.black, elevation: 0, - shadowColor: Colors.transparent, + backgroundColor: _violetL, + foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 18, + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: Colors.black, - width: 3, - ), + borderRadius: BorderRadius.circular(AppRadii.md), ), - textStyle: const TextStyle( - fontWeight: FontWeight.w900, - fontSize: 16, + textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black87, + side: const BorderSide(color: _outlineL, width: 1.4), + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + ), + ), + + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.black54, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), ), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: const Color(0xFF242424), - contentPadding: const EdgeInsets.all(18), + fillColor: _surfaceRaisedL, + hintStyle: const TextStyle(color: Colors.black38), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.lg, + ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide( - color: Colors.white, - width: 3, - ), + borderRadius: BorderRadius.circular(AppRadii.md), + borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide( - color: Colors.white, - width: 3, - ), + borderRadius: BorderRadius.circular(AppRadii.md), + borderSide: const BorderSide(color: _outlineL), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide( - color: Color(0xFFFFD60A), - width: 4, - ), + borderRadius: BorderRadius.circular(AppRadii.md), + borderSide: const BorderSide(color: _violetL, width: 2), ), ), - floatingActionButtonTheme: const FloatingActionButtonThemeData( - backgroundColor: Color(0xFFFF5252), - foregroundColor: Colors.white, - elevation: 0, + dialogTheme: DialogThemeData( + backgroundColor: _surfaceL, + surfaceTintColor: Colors.transparent, + elevation: 8, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - side: BorderSide( - color: Colors.white, - width: 3, - ), + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + titleTextStyle: const TextStyle( + color: Colors.black87, + fontSize: 19, + fontWeight: FontWeight.w800, + ), + contentTextStyle: const TextStyle(color: Colors.black54, height: 1.4), + ), + + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: _violetL, + foregroundColor: Colors.white, + elevation: 2, + ), + + navigationBarTheme: NavigationBarThemeData( + backgroundColor: _surfaceL, + indicatorColor: _violetL.withOpacity(0.15), + labelTextStyle: const WidgetStatePropertyAll( + TextStyle(fontWeight: FontWeight.w600), + ), + ), + + listTileTheme: ListTileThemeData( + tileColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + iconColor: _violetSoftL, + textColor: Colors.black87, + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, ), ), snackBarTheme: SnackBarThemeData( - backgroundColor: const Color(0xFF242424), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: Colors.white, - width: 3, - ), - ), + backgroundColor: Colors.black87, behavior: SnackBarBehavior.floating, - ), - - dividerTheme: const DividerThemeData( - color: Colors.white, - thickness: 3, - ), - - listTileTheme: ListTileThemeData( - tileColor: const Color(0xFF242424), + elevation: 4, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: Colors.white, - width: 3, - ), + borderRadius: BorderRadius.circular(AppRadii.md), ), - textColor: Colors.white, - iconColor: const Color(0xFFFFD60A), + contentTextStyle: const TextStyle(color: Colors.white), + ), + + dividerTheme: const DividerThemeData(color: _outlineL, thickness: 1, space: 1), + + chipTheme: ChipThemeData( + backgroundColor: _surfaceRaisedL, + labelStyle: const TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.sm), + side: const BorderSide(color: _outlineL), + ), + ), + + textTheme: const TextTheme( + displayLarge: TextStyle(color: Colors.black87, fontWeight: FontWeight.w800), + headlineMedium: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, + ), + titleLarge: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w800, + letterSpacing: -0.2, + ), + titleMedium: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + bodyLarge: TextStyle(color: Colors.black54, fontSize: 16, height: 1.5), + bodyMedium: TextStyle(color: Colors.black45, height: 1.4), + bodySmall: TextStyle(color: Colors.black38, height: 1.3), + labelLarge: TextStyle(color: Colors.black87, fontWeight: FontWeight.w700), ), ); \ No newline at end of file diff --git a/lib/viewmodels/bar_screen_view_model.dart b/lib/viewmodels/bar_screen_view_model.dart index b062614..e8b67c3 100644 --- a/lib/viewmodels/bar_screen_view_model.dart +++ b/lib/viewmodels/bar_screen_view_model.dart @@ -114,17 +114,12 @@ class BarScreenViewModel extends ChangeNotifier { await barTabService.closeTab(tab.id); - _selectedTabId = null; await _reloadTabs(); } Future closeTab(String tabId) async { await barTabService.closeTab(tabId); - if (_selectedTabId == tabId) { - _selectedTabId = null; - } - await _reloadTabs(); } diff --git a/lib/viewmodels/history_view_model.dart b/lib/viewmodels/history_view_model.dart new file mode 100644 index 0000000..fccd861 --- /dev/null +++ b/lib/viewmodels/history_view_model.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; + +import '../models/closed_tab.dart'; +import '../services/bar_tab_service.dart'; + +class HistoryViewModel extends ChangeNotifier { + final BarTabService barTabService; + + HistoryViewModel({ + required this.barTabService, + }); + + List _closedTabs = []; + bool _isLoading = false; + bool _hasLoaded = false; + String? _errorMessage; + String _searchQuery = ''; + + List get closedTabs { + if (_searchQuery.isEmpty) return _closedTabs; + + return _closedTabs + .where((tab) => + tab.customerName.toLowerCase().contains(_searchQuery.toLowerCase())) + .toList(); + } + + bool get isLoading => _isLoading; + + bool get hasLoaded => _hasLoaded; + + String? get errorMessage => _errorMessage; + + Future ensureLoaded() async { + if (_hasLoaded || _isLoading) return; + + await load(); + } + + Future load() async { + if (_isLoading) return; + + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + try { + _closedTabs = await barTabService.getClosedTabs(); + } catch (_) { + _errorMessage = 'Could not load tab history.'; + } finally { + _hasLoaded = true; + _isLoading = false; + notifyListeners(); + } + } + + void search(String query) { + _searchQuery = query; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/views/bar_screen_view.dart b/lib/views/bar_screen_view.dart index 63d6852..a4484f1 100644 --- a/lib/views/bar_screen_view.dart +++ b/lib/views/bar_screen_view.dart @@ -22,34 +22,65 @@ class BarScreenView extends StatelessWidget { return Scaffold( appBar: AppBar( + title: const Text('Bar Tabs'), actions: [ IconButton( tooltip: 'Manage products', onPressed: () => context.go('/products'), icon: const Icon(Icons.inventory_2_outlined), ), + const SizedBox(width: 6), + IconButton( + tooltip: 'Tab history', + onPressed: () => context.go('/history'), + icon: const Icon(Icons.history_rounded), + ), + const SizedBox(width: 6), IconButton( tooltip: 'Refresh', onPressed: viewModel.load, - icon: const Icon(Icons.refresh), + icon: const Icon(Icons.refresh_rounded), ), + const SizedBox(width: 8), ], ), resizeToAvoidBottomInset: false, body: Builder( builder: (context) { if (viewModel.isLoading) { - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator(strokeWidth: 2.5), + ); } if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline_rounded, + size: 40, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 12), + Text( + viewModel.errorMessage!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + ); } return Row( children: [ Expanded( - flex: 3, + flex: 2, child: _ProductGrid( products: productsViewModel.products, hasSelectedTab: viewModel.selectedTab != null, @@ -67,7 +98,7 @@ class BarScreenView extends StatelessWidget { }, ), ), - const VerticalDivider(width: 1), + Container(width: 1, color: Theme.of(context).dividerColor), Expanded( flex: 1, child: _TabPanel( @@ -101,12 +132,12 @@ class BarScreenView extends StatelessWidget { autofocus: true, decoration: const InputDecoration( labelText: 'Customer / group name', - border: OutlineInputBorder(), ), onSubmitted: (value) { Navigator.of(dialogContext).pop(value); }, ), + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), @@ -141,16 +172,17 @@ class BarScreenView extends StatelessWidget { builder: (dialogContext) { return AlertDialog( title: Text('Close ${tab.customerName}ʼs tab?'), - content: Text( - 'Current total: ${tab.formattedTotal}\n\n' - 'Payments are not handled yet, so this only marks the tab as closed.', - ), + content: Text('Current total: ${tab.formattedTotal}\n\n'), + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel'), ), FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(dialogContext).colorScheme.error, + ), onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Close tab'), ), @@ -165,6 +197,10 @@ class BarScreenView extends StatelessWidget { } } +// --------------------------------------------------------------------------- +// Product grid +// --------------------------------------------------------------------------- + class _ProductGrid extends StatelessWidget { final List products; final bool hasSelectedTab; @@ -178,15 +214,36 @@ class _ProductGrid extends StatelessWidget { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + if (products.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.inventory_2_outlined, size: 48), - const SizedBox(height: 12), - const Text('No products yet.'), - const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: scheme.onSurface.withOpacity(0.05), + ), + child: Icon( + Icons.inventory_2_outlined, + size: 40, + color: scheme.onSurface.withOpacity(0.3), + ), + ), + const SizedBox(height: 16), + Text( + 'No products yet', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + 'Add your first product to start selling.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.go('/products/new'), icon: const Icon(Icons.add), @@ -197,32 +254,79 @@ class _ProductGrid extends StatelessWidget { ); } - return LayoutBuilder( - builder: (context, constraints) { - final columns = ((constraints.maxWidth / 170).floor()) - .clamp(3, 6) - .toInt(); - - return GridView.builder( - padding: const EdgeInsets.all(16), - itemCount: products.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 1, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 4), + child: Row( + children: [ + Text('Products', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(width: 10), + if (!hasSelectedTab) + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: scheme.onSurface.withOpacity(0.06), + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.info_outline_rounded, + size: 14, + color: scheme.onSurface.withOpacity(0.5), + ), + const SizedBox(width: 4), + Flexible( + child: Text( + 'Select a tab to add items', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), + ), + ), + ], ), - itemBuilder: (context, index) { - final product = products[index]; + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final columns = ((constraints.maxWidth / 170).floor()) + .clamp(3, 5) + .toInt(); - return _ProductTile( - product: product, - enabled: hasSelectedTab, - onTap: () => onProductTap(product), - ); - }, - ); - }, + return GridView.builder( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + itemCount: products.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 1, + ), + itemBuilder: (context, index) { + final product = products[index]; + + return _ProductTile( + product: product, + enabled: hasSelectedTab, + onTap: () => onProductTap(product), + ); + }, + ); + }, + ), + ), + ], ); } } @@ -250,41 +354,90 @@ class _ProductTile extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - clipBehavior: Clip.antiAlias, - margin: EdgeInsets.zero, - child: InkWell( - onTap: enabled ? onTap : null, - child: Opacity( - opacity: enabled ? 1 : 0.45, - child: _hasImage - ? Image.file( - File(product.imagePath!), - fit: BoxFit.scaleDown, - width: double.infinity, - height: double.infinity, - ) - : const Center( - child: Stack( - children: [ - Icon(Icons.image_not_supported_outlined, size: 42), - Text("No Image found!"), - ], + final theme = Theme.of(context); + final scheme = theme.colorScheme; + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: scheme.onSurface.withOpacity(0.08), width: 1), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Material( + color: theme.cardTheme.color ?? scheme.surface, + child: InkWell( + onTap: enabled ? onTap : null, + child: Opacity( + opacity: enabled ? 1 : 0.4, + child: Stack( + fit: StackFit.expand, + children: [ + _hasImage + ? Image.file( + File(product.imagePath!), + fit: BoxFit.scaleDown, + width: double.infinity, + height: double.infinity, + ) + : Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.image_not_supported_outlined, + size: 34, + color: scheme.onSurface.withOpacity(0.3), + ), + const SizedBox(height: 6), + Text( + 'No image', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), - ), + // Scrim stays black regardless of theme — it's for legibility + // of the (usually light/photographic) image beneath it, not + // themed UI chrome. + if (_hasImage) + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.35), + ], + stops: const [0.6, 1.0], + ), + ), + ), + ), + ], + ), + ), + ), ), ), ); } } +// --------------------------------------------------------------------------- +// Tab panel +// --------------------------------------------------------------------------- + class _TabPanel extends StatefulWidget { final List tabs; final BarTab? selectedTab; final String? selectedTabId; final VoidCallback onNewTabPressed; final ValueChanged onTabSelected; - final Future Function(TabItem item, int quantity) onItemQuantityChanged; + final Future Function(TabItem item, int quantity) + onItemQuantityChanged; final VoidCallback onCloseTabPressed; final ValueChanged onTabClosed; @@ -315,79 +468,130 @@ class _TabPanelState extends State<_TabPanel> { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final filteredTabs = widget.tabs.where((tab) { return tab.customerName.toLowerCase().contains( _searchQuery.toLowerCase(), ); }).toList(); - return Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: widget.onNewTabPressed, - icon: const Icon(Icons.add), - label: const Text('Open tab'), - ), - ), - - const SizedBox(height: 12), - - TextField( - controller: _searchController, - onChanged: (value) { - setState(() { - _searchQuery = value; - }); - }, - decoration: const InputDecoration( - hintText: 'Search name...', - prefixIcon: Icon(Icons.search), - ), - ), - - const SizedBox(height: 12), - - Align( - alignment: Alignment.centerLeft, - child: Text( - 'Open tabs', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - - const SizedBox(height: 8), - - SizedBox( - height: 200, - child: _OpenTabsList( - tabs: filteredTabs, - selectedTabId: widget.selectedTabId, - onTabSelected: widget.onTabSelected, - onTabClosed: widget.onTabClosed, - ), - ), - - const Divider(height: 24), - - Expanded( - child: widget.selectedTab == null - ? const Center(child: Text('Select or open a tab.')) - : _SelectedTabDetails( - tab: widget.selectedTab!, - onItemQuantityChanged: widget.onItemQuantityChanged, - onCloseTabPressed: widget.onCloseTabPressed, + return DecoratedBox( + // A step darker/lighter than the main surface, whichever direction + // the active theme goes — matches how it read against the dark + // surface color originally. + decoration: BoxDecoration(color: scheme.surfaceContainerLow), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + decoration: const InputDecoration( + hintText: 'Search by name…', + prefixIcon: Icon(Icons.search_rounded, size: 20), + isDense: true, + ), ), - ), - ], + ), + + const SizedBox(width: 10), + + IconButton.filled( + onPressed: widget.onNewTabPressed, + icon: const Icon(Icons.add_rounded), + tooltip: 'Open new tab', + ), + ], + ), + + const SizedBox(height: 18), + + Row( + children: [ + Text( + 'OPEN TABS', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: scheme.onSurface.withOpacity(0.06), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '${filteredTabs.length}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + + const SizedBox(height: 8), + + SizedBox( + height: 190, + child: _OpenTabsList( + tabs: filteredTabs, + selectedTabId: widget.selectedTabId, + onTabSelected: widget.onTabSelected, + onTabClosed: widget.onTabClosed, + ), + ), + + const Divider(height: 28), + + Expanded( + child: widget.selectedTab == null + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.receipt_long_outlined, + size: 36, + color: scheme.onSurface.withOpacity(0.25), + ), + const SizedBox(height: 10), + Text( + 'Select or open a tab', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ) + : _SelectedTabDetails( + tab: widget.selectedTab!, + onItemQuantityChanged: widget.onItemQuantityChanged, + onCloseTabPressed: widget.onCloseTabPressed, + ), + ), + ], + ), ), ); } } + class _OpenTabsList extends StatelessWidget { final List tabs; final String? selectedTabId; @@ -404,22 +608,30 @@ class _OpenTabsList extends StatelessWidget { @override Widget build(BuildContext context) { if (tabs.isEmpty) { - return const Center(child: Text('No open tabs.')); + return Center( + child: Text( + 'No open tabs', + style: Theme.of(context).textTheme.bodyMedium, + ), + ); } return ListView.separated( itemCount: tabs.length, - separatorBuilder: (_, __) => const SizedBox(height: 6), + separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final tab = tabs[index]; final selected = tab.id == selectedTabId; + final scheme = Theme.of(context).colorScheme; + final primary = scheme.primary; return ClipRRect( + borderRadius: BorderRadius.circular(14), child: Slidable( key: ValueKey(tab.id), endActionPane: ActionPane( motion: const DrawerMotion(), - extentRatio: 0.65, + extentRatio: 0.6, children: [ SlidableAction( onPressed: (_) { @@ -433,28 +645,63 @@ class _OpenTabsList extends StatelessWidget { ), SlidableAction( onPressed: (_) => onTabClosed(tab.id), - icon: Icons.close, + icon: Icons.close_rounded, label: 'Close', backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), ], ), - child: Card( - margin: EdgeInsets.zero, - color: selected - ? Theme.of(context).colorScheme.primaryContainer - : null, - child: ListTile( - dense: true, - selected: selected, - title: Text( - tab.customerName, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: selected + ? primary.withOpacity(0.14) + : scheme.onSurface.withOpacity(0.04), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: selected + ? primary.withOpacity(0.6) + : scheme.onSurface.withOpacity(0.06), + width: selected ? 1.4 : 1, + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () => onTabSelected(tab.id), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + tab.customerName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: scheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + '${tab.itemCount} items - ${tab.formattedTotal}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), ), - subtitle: Text('${tab.itemCount} items • ${tab.formattedTotal}'), - onTap: () => onTabSelected(tab.id), ), ), ), @@ -464,6 +711,9 @@ class _OpenTabsList extends StatelessWidget { } } + + + class _SelectedTabDetails extends StatelessWidget { final BarTab tab; final Future Function(TabItem item, int quantity) onItemQuantityChanged; @@ -477,52 +727,89 @@ class _SelectedTabDetails extends StatelessWidget { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - tab.customerName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 4), - Text( - 'Total: ${tab.formattedTotal}', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 12), - Expanded( - child: tab.items.isEmpty - ? const Center(child: Text('Tap products to add them.')) - : ListView.separated( - itemCount: tab.items.length, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, index) { - final item = tab.items[index]; - - return _TabItemRow( - item: item, - onQuantityChanged: onItemQuantityChanged, - ); - }, - ), - ), - const Divider(height: 24), Row( children: [ Expanded( child: Text( - tab.formattedTotal, - style: Theme.of(context).textTheme.headlineSmall, + tab.customerName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge, ), ), - FilledButton( - onPressed: onCloseTabPressed, - child: const Text('Close'), - ), + Divider() ], ), + const SizedBox(height: 16), + Expanded( + child: tab.items.isEmpty + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.local_bar_outlined, + size: 32, + color: scheme.onSurface.withOpacity(0.25), + ), + const SizedBox(height: 8), + Text( + 'Tap products to add them', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ) + : ListView.separated( + itemCount: tab.items.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final item = tab.items[index]; + + return _TabItemRow( + item: item, + onQuantityChanged: onItemQuantityChanged, + ); + }, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.12), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Total', style: Theme.of(context).textTheme.bodySmall), + Text( + tab.formattedTotal, + style: Theme.of(context).textTheme.headlineMedium + ?.copyWith(fontSize: 24), + ), + ], + ), + ), + FilledButton( + onPressed: onCloseTabPressed, + child: const Text('Close tab'), + ), + ], + ), + ), ], ); } @@ -536,8 +823,10 @@ class _TabItemRow extends StatelessWidget { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 10), child: Row( children: [ Expanded( @@ -548,7 +837,9 @@ class _TabItemRow extends StatelessWidget { item.productName, maxLines: 1, overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600), ), + const SizedBox(height: 2), Text( '${item.quantity} × ${item.formattedUnitPrice}', style: Theme.of(context).textTheme.bodySmall, @@ -556,23 +847,47 @@ class _TabItemRow extends StatelessWidget { ], ), ), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: () => onQuantityChanged(item, item.quantity - 1), - icon: const Icon(Icons.remove_circle_outline), - ), - Text('${item.quantity}'), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: () => onQuantityChanged(item, item.quantity + 1), - icon: const Icon(Icons.add_circle_outline), + Container( + decoration: BoxDecoration( + color: scheme.onSurface.withOpacity(0.05), + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () => + onQuantityChanged(item, item.quantity - 1), + icon: const Icon(Icons.remove_rounded, size: 18), + ), + SizedBox( + width: 22, + child: Text( + '${item.quantity}', + textAlign: TextAlign.center, + style: const TextStyle(fontWeight: FontWeight.w700), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () => + onQuantityChanged(item, item.quantity + 1), + icon: const Icon(Icons.add_rounded, size: 18), + ), + ], + ), ), SizedBox( width: 72, - child: Text(item.formattedLineTotal, textAlign: TextAlign.end), + child: Text( + item.formattedLineTotal, + textAlign: TextAlign.end, + style: const TextStyle(fontWeight: FontWeight.w700), + ), ), ], ), ); } -} +} \ No newline at end of file diff --git a/lib/views/history_screen_view.dart b/lib/views/history_screen_view.dart new file mode 100644 index 0000000..5b3e09b --- /dev/null +++ b/lib/views/history_screen_view.dart @@ -0,0 +1,329 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import '../app/router.dart'; +import '../models/closed_tab.dart'; +import '../models/closed_tab_item.dart'; +import '../viewmodels/history_view_model.dart'; + +class HistoryScreenView extends StatefulWidget { + const HistoryScreenView({super.key}); + + @override + State createState() => _HistoryScreenViewState(); +} + +class _HistoryScreenViewState extends State with RouteAware { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().ensureLoaded(); + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().load(); + }); + } + + @override + void didPopNext() { + // Called when you come back to this screen + context.read().load(); + } + + @override + void dispose() { + routeObserver.unsubscribe(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + + return Scaffold( + appBar: AppBar( + title: Row( + children: [ + IconButton(onPressed: (){ + context.go('/bar'); + }, icon: const Icon(Icons.arrow_back)), + const SizedBox(width: 5), + const Text('Tab History'), + ], + ), + actions: [ + IconButton( + tooltip: 'Refresh', + onPressed: viewModel.load, + icon: const Icon(Icons.refresh_rounded), + ), + const SizedBox(width: 8), + ], + ), + body: Builder( + builder: (context) { + if (viewModel.isLoading && !viewModel.hasLoaded) { + return const Center( + child: CircularProgressIndicator(strokeWidth: 2.5), + ); + } + + if (viewModel.errorMessage != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline_rounded, + size: 40, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 12), + Text( + viewModel.errorMessage!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + ); + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), + child: TextField( + onChanged: viewModel.search, + decoration: const InputDecoration( + hintText: 'Search by name…', + prefixIcon: Icon(Icons.search_rounded, size: 20), + isDense: true, + ), + ), + ), + Expanded( + child: viewModel.closedTabs.isEmpty + ? _EmptyState() + : ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + itemCount: viewModel.closedTabs.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final closedTab = viewModel.closedTabs[index]; + + return _ClosedTabCard(closedTab: closedTab); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: scheme.onSurface.withOpacity(0.05), + ), + child: Icon( + Icons.history_rounded, + size: 40, + color: scheme.onSurface.withOpacity(0.3), + ), + ), + const SizedBox(height: 16), + Text( + 'No closed tabs yet', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + 'Tabs you close will show up here.', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ); + } +} + +class _ClosedTabCard extends StatefulWidget { + final ClosedTab closedTab; + + const _ClosedTabCard({required this.closedTab}); + + @override + State<_ClosedTabCard> createState() => _ClosedTabCardState(); +} + +class _ClosedTabCardState extends State<_ClosedTabCard> { + bool _expanded = false; + + @override + Widget build(BuildContext context) { + final closedTab = widget.closedTab; + final dateFormat = DateFormat('MMM d, y · h:mm a'); + final scheme = Theme.of(context).colorScheme; + + return Container( + decoration: BoxDecoration( + color: scheme.onSurface.withOpacity(0.04), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: scheme.onSurface.withOpacity(0.06)), + ), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => _expanded = !_expanded), + enableFeedback: true, + splashFactory: NoSplash.splashFactory, + + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + closedTab.customerName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: scheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + '${dateFormat.format(closedTab.closedAt)} · ${closedTab.itemCount} items', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + const SizedBox(width: 8), + Text( + closedTab.formattedTotal, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: scheme.onSurface, + ), + ), + const SizedBox(width: 4), + Icon( + _expanded + ? Icons.expand_less_rounded + : Icons.expand_more_rounded, + color: scheme.onSurface.withOpacity(0.5), + ), + ], + ), + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 150), + crossFadeState: _expanded + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 14), + child: Column( + children: [ + const Divider(height: 1), + const SizedBox(height: 8), + ...closedTab.items.map( + (item) => _ClosedTabItemRow(item: item), + ), + ], + ), + ), + secondChild: const SizedBox(width: double.infinity), + ), + ], + ), + ), + ), + ); + } +} + +class _ClosedTabItemRow extends StatelessWidget { + final ClosedTabItem item; + + const _ClosedTabItemRow({required this.item}); + + @override + Widget build(BuildContext context) { + final unitPrice = + NumberFormat.simpleCurrency().format(item.unitPriceInCents / 100); + final lineTotal = + NumberFormat.simpleCurrency().format(item.lineTotalInCents / 100); + final scheme = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + child: Text( + item.productName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontWeight: FontWeight.w600, color: scheme.onSurface), + ), + ), + Text( + '${item.quantity} × $unitPrice', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(width: 12), + SizedBox( + width: 64, + child: Text( + lineTotal, + textAlign: TextAlign.end, + style: TextStyle(fontWeight: FontWeight.w700, color: scheme.onSurface), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 7fbe8d3..4988a92 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b + url: "https://pub.dev" + source: hosted + version: "100.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + url: "https://pub.dev" + source: hosted + version: "13.0.0" args: dependency: transitive description: @@ -25,6 +41,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + build_config: + dependency: transitive + description: + name: build_config + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae + url: "https://pub.dev" + source: hosted + version: "1.3.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + url: "https://pub.dev" + source: hosted + version: "4.1.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" characters: dependency: transitive description: @@ -33,6 +97,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: "5909d2c6b66817222779e1eedc19e0e28b76d1df7bd9856a4792ccb9881df358" + url: "https://pub.dev" + source: hosted + version: "0.5.1" clock: dependency: transitive description: @@ -89,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" + url: "https://pub.dev" + source: hosted + version: "3.1.9" drift: dependency: "direct main" description: @@ -97,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.34.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "0994276f63a394b7434ed7deaeffd2ddc855a5eafccf5ed00e5e341905a6f62b" + url: "https://pub.dev" + source: hosted + version: "2.34.3" drift_flutter: dependency: "direct main" description: @@ -224,6 +328,14 @@ packages: url: "https://pub.dev" source: hosted version: "17.3.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" hooks: dependency: transitive description: @@ -240,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -312,6 +432,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" jni: dependency: transitive description: @@ -328,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" leak_tracker: dependency: transitive description: @@ -504,6 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" provider: dependency: "direct main" description: @@ -520,6 +672,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" record_use: dependency: transitive description: @@ -528,11 +696,35 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + url: "https://pub.dev" + source: hosted + version: "4.2.3" source_span: dependency: transitive description: @@ -565,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0+eol" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d" + url: "https://pub.dev" + source: hosted + version: "0.45.0" stack_trace: dependency: transitive description: @@ -581,6 +781,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -637,6 +845,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: @@ -645,6 +861,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d88f527..db0dbdd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: image_picker: ^1.2.3 path: ^1.9.1 flutter_slidable: ^4.0.3 + intl: ^0.20.3 + dev_dependencies: flutter_test: @@ -54,6 +56,8 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 + drift_dev: ^2.34.2+1 + build_runner: ^2.15.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec