From 14fcb9a53370c3b5bbadf9ce3b653d59419e759a Mon Sep 17 00:00:00 2001 From: gitea Date: Mon, 31 Aug 2026 17:03:03 +0200 Subject: [PATCH] fixes before build --- lib/home_screen.dart | 6 +++++- lib/models/product.dart | 2 +- lib/models/supermarket.dart | 26 +++++++++++++++++++++++++ lib/products_screen.dart | 19 +++++++++++++----- lib/services/product_repository.dart | 19 +++++++++++++++++- test/catalog_test.dart | 19 ++++++++++++++++++ test/supermarket_test.dart | 29 ++++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 8 deletions(-) diff --git a/lib/home_screen.dart b/lib/home_screen.dart index 1e5ef13..83c333a 100644 --- a/lib/home_screen.dart +++ b/lib/home_screen.dart @@ -63,10 +63,11 @@ class _HomeScreenState extends State { try { final overview = await widget.repository.getOverview(); final stores = await widget.repository.getSupermarkets(); + final lastTripStores = await widget.repository.getLastTripSupermarkets(); if (!mounted || token != _loadToken) return; setState(() { _overview = overview; - _stores = stores; + _stores = sortStoresByLastTrip(stores, lastTripStores); _error = null; }); } catch (error) { @@ -281,6 +282,9 @@ class _HomeBody extends StatelessWidget { SizedBox( height: 148, child: ListView.separated( + key: ValueKey( + stores.isEmpty ? 'bonus-empty' : 'bonus-${stores.first.name}', + ), scrollDirection: Axis.horizontal, itemCount: stores.length, separatorBuilder: (_, _) => const SizedBox(width: 12), diff --git a/lib/models/product.dart b/lib/models/product.dart index 7a9edcd..78ce466 100644 --- a/lib/models/product.dart +++ b/lib/models/product.dart @@ -72,4 +72,4 @@ class Product { } } -enum ProductSort { name, preferred, start } +enum ProductSort { name, preferred, favourite } diff --git a/lib/models/supermarket.dart b/lib/models/supermarket.dart index d11064e..8267e25 100644 --- a/lib/models/supermarket.dart +++ b/lib/models/supermarket.dart @@ -50,6 +50,32 @@ class Supermarket { } } +/// Puts stores with the most recent trip first, then earlier trips, then unused. +List sortStoresByLastTrip( + List stores, + Iterable lastTripStores, +) { + final rank = {}; + var next = 0; + for (final name in lastTripStores) { + final key = name.trim().toUpperCase(); + if (key.isEmpty || rank.containsKey(key)) continue; + rank[key] = next++; + } + + final sorted = [...stores]; + sorted.sort((a, b) { + final aRank = rank[a.name.trim().toUpperCase()] ?? 1 << 20; + final bRank = rank[b.name.trim().toUpperCase()] ?? 1 << 20; + final byTrip = aRank.compareTo(bRank); + if (byTrip != 0) return byTrip; + final byOrder = a.sortOrder.compareTo(b.sortOrder); + if (byOrder != 0) return byOrder; + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + return sorted; +} + /// Palette offered when the user picks a store color. const List kStoreColorPalette = [ 0xFFEEC21B, // Jumbo yellow diff --git a/lib/products_screen.dart b/lib/products_screen.dart index f7eacf8..e9365a2 100644 --- a/lib/products_screen.dart +++ b/lib/products_screen.dart @@ -180,10 +180,16 @@ class _ProductsScreenState extends State { for (final item in _products ?? const []) if (item.id == product.id) item.copyWith(favourite: next) else item, ]; + if (_sort == ProductSort.favourite && !next) { + _products = [ + for (final item in _products!) + if (item.id != product.id) item, + ]; + if (_products!.isEmpty) _editing = false; + } }); try { await widget.repository.setFavourite(product.id, next); - if (_sort == ProductSort.start && mounted) _load(); } catch (_) { if (mounted) _load(); } @@ -265,11 +271,12 @@ class _ProductsScreenState extends State { ), ButtonSegment( value: ProductSort.preferred, - label: Text('Preferred'), + label: Text('Frequent'), ), ButtonSegment( - value: ProductSort.start, - label: Text('Start'), + value: ProductSort.favourite, + icon: Icon(Icons.star), + label: Text('Star'), ), ], selected: {_sort}, @@ -311,7 +318,9 @@ class _ProductsScreenState extends State { if (_products!.isEmpty) { return Center( child: Text( - 'Confirmed receipt items will show up here.', + _sort == ProductSort.favourite + ? 'Star products to keep them as favourites.' + : 'Confirmed receipt items will show up here.', textAlign: TextAlign.center, style: theme.textTheme.bodyLarge, ), diff --git a/lib/services/product_repository.dart b/lib/services/product_repository.dart index 4899335..c414e00 100644 --- a/lib/services/product_repository.dart +++ b/lib/services/product_repository.dart @@ -392,10 +392,11 @@ class ProductRepository extends ChangeNotifier { ProductSort sort = ProductSort.name, }) async { final db = await _database; + final favouritesOnly = sort == ProductSort.favourite; final orderBy = switch (sort) { ProductSort.name => 'name COLLATE NOCASE ASC', ProductSort.preferred => 'times_seen DESC, name COLLATE NOCASE ASC', - ProductSort.start => 'favourite DESC, name COLLATE NOCASE ASC', + ProductSort.favourite => 'name COLLATE NOCASE ASC', }; final rows = await db.rawQuery(''' SELECT @@ -409,6 +410,7 @@ class ProductRepository extends ChangeNotifier { LIMIT 1 ) AS display_price FROM products + ${favouritesOnly ? 'WHERE favourite = 1' : ''} ORDER BY $orderBy '''); return rows.map(Product.fromMap).toList(); @@ -1436,6 +1438,21 @@ class ProductRepository extends ChangeNotifier { return rows.map(Supermarket.fromMap).toList(); } + /// Store names from newest trip to oldest, unique. + Future> getLastTripSupermarkets() async { + final db = await _database; + final rows = await db.rawQuery(''' + SELECT supermarket + FROM shops + GROUP BY supermarket + ORDER BY MAX(shopped_at) DESC, MAX(id) DESC + '''); + return [ + for (final row in rows) + if (row['supermarket'] is String) row['supermarket']! as String, + ]; + } + Future addSupermarket({ required String name, required int colorValue, diff --git a/test/catalog_test.dart b/test/catalog_test.dart index ca72d3a..7ea4a8c 100644 --- a/test/catalog_test.dart +++ b/test/catalog_test.dart @@ -52,6 +52,25 @@ void main() { }); }); + test('getLastTripSupermarkets lists unique stores newest first', () async { + final repository = memoryRepository(); + await repository.saveShop( + supermarket: 'Jumbo', + shoppedAt: DateTime(2026, 8, 1), + items: [LineItem(id: '1', name: 'Milk', price: 1.5)], + ); + await repository.saveShop( + supermarket: 'Albert Heijn', + shoppedAt: DateTime(2026, 8, 12), + items: [LineItem(id: '2', name: 'Bread', price: 2)], + ); + + expect(await repository.getLastTripSupermarkets(), [ + 'Albert Heijn', + 'Jumbo', + ]); + }); + test('deleting all products leaves shopping trips in place', () async { final repository = memoryRepository(); await repository.saveShop( diff --git a/test/supermarket_test.dart b/test/supermarket_test.dart index 9dc3eeb..87b064a 100644 --- a/test/supermarket_test.dart +++ b/test/supermarket_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:receipity/models/supermarket.dart'; import 'package:receipity/utils/supermarkets.dart'; void main() { @@ -20,4 +21,32 @@ void main() { test('returns null when no supermarket is present', () { expect(detectSupermarket('Milk 1.50\nBread 2.00'), isNull); }); + + test('sortStoresByLastTrip puts the latest trip store first', () { + const jumbo = Supermarket(id: 1, name: 'Jumbo', colorValue: 1, sortOrder: 0); + const lidl = Supermarket(id: 2, name: 'Lidl', colorValue: 2, sortOrder: 1); + const ah = Supermarket( + id: 3, + name: 'Albert Heijn', + colorValue: 3, + sortOrder: 2, + ); + + final afterJumbo = sortStoresByLastTrip([jumbo, lidl, ah], ['Jumbo']); + expect(afterJumbo.map((store) => store.name), [ + 'Jumbo', + 'Lidl', + 'Albert Heijn', + ]); + + final afterAh = sortStoresByLastTrip( + [jumbo, lidl, ah], + ['Albert Heijn', 'Jumbo'], + ); + expect(afterAh.map((store) => store.name), [ + 'Albert Heijn', + 'Jumbo', + 'Lidl', + ]); + }); }