fixes before build

This commit is contained in:
gitea
2026-08-31 17:03:03 +02:00
parent 756f9db356
commit 14fcb9a533
7 changed files with 112 additions and 8 deletions
+5 -1
View File
@@ -63,10 +63,11 @@ class _HomeScreenState extends State<HomeScreen> {
try { try {
final overview = await widget.repository.getOverview(); final overview = await widget.repository.getOverview();
final stores = await widget.repository.getSupermarkets(); final stores = await widget.repository.getSupermarkets();
final lastTripStores = await widget.repository.getLastTripSupermarkets();
if (!mounted || token != _loadToken) return; if (!mounted || token != _loadToken) return;
setState(() { setState(() {
_overview = overview; _overview = overview;
_stores = stores; _stores = sortStoresByLastTrip(stores, lastTripStores);
_error = null; _error = null;
}); });
} catch (error) { } catch (error) {
@@ -281,6 +282,9 @@ class _HomeBody extends StatelessWidget {
SizedBox( SizedBox(
height: 148, height: 148,
child: ListView.separated( child: ListView.separated(
key: ValueKey(
stores.isEmpty ? 'bonus-empty' : 'bonus-${stores.first.name}',
),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: stores.length, itemCount: stores.length,
separatorBuilder: (_, _) => const SizedBox(width: 12), separatorBuilder: (_, _) => const SizedBox(width: 12),
+1 -1
View File
@@ -72,4 +72,4 @@ class Product {
} }
} }
enum ProductSort { name, preferred, start } enum ProductSort { name, preferred, favourite }
+26
View File
@@ -50,6 +50,32 @@ class Supermarket {
} }
} }
/// Puts stores with the most recent trip first, then earlier trips, then unused.
List<Supermarket> sortStoresByLastTrip(
List<Supermarket> stores,
Iterable<String> lastTripStores,
) {
final rank = <String, int>{};
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. /// Palette offered when the user picks a store color.
const List<int> kStoreColorPalette = [ const List<int> kStoreColorPalette = [
0xFFEEC21B, // Jumbo yellow 0xFFEEC21B, // Jumbo yellow
+14 -5
View File
@@ -180,10 +180,16 @@ class _ProductsScreenState extends State<ProductsScreen> {
for (final item in _products ?? const <Product>[]) for (final item in _products ?? const <Product>[])
if (item.id == product.id) item.copyWith(favourite: next) else item, 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 { try {
await widget.repository.setFavourite(product.id, next); await widget.repository.setFavourite(product.id, next);
if (_sort == ProductSort.start && mounted) _load();
} catch (_) { } catch (_) {
if (mounted) _load(); if (mounted) _load();
} }
@@ -265,11 +271,12 @@ class _ProductsScreenState extends State<ProductsScreen> {
), ),
ButtonSegment( ButtonSegment(
value: ProductSort.preferred, value: ProductSort.preferred,
label: Text('Preferred'), label: Text('Frequent'),
), ),
ButtonSegment( ButtonSegment(
value: ProductSort.start, value: ProductSort.favourite,
label: Text('Start'), icon: Icon(Icons.star),
label: Text('Star'),
), ),
], ],
selected: {_sort}, selected: {_sort},
@@ -311,7 +318,9 @@ class _ProductsScreenState extends State<ProductsScreen> {
if (_products!.isEmpty) { if (_products!.isEmpty) {
return Center( return Center(
child: Text( 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, textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge, style: theme.textTheme.bodyLarge,
), ),
+18 -1
View File
@@ -392,10 +392,11 @@ class ProductRepository extends ChangeNotifier {
ProductSort sort = ProductSort.name, ProductSort sort = ProductSort.name,
}) async { }) async {
final db = await _database; final db = await _database;
final favouritesOnly = sort == ProductSort.favourite;
final orderBy = switch (sort) { final orderBy = switch (sort) {
ProductSort.name => 'name COLLATE NOCASE ASC', ProductSort.name => 'name COLLATE NOCASE ASC',
ProductSort.preferred => 'times_seen DESC, 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(''' final rows = await db.rawQuery('''
SELECT SELECT
@@ -409,6 +410,7 @@ class ProductRepository extends ChangeNotifier {
LIMIT 1 LIMIT 1
) AS display_price ) AS display_price
FROM products FROM products
${favouritesOnly ? 'WHERE favourite = 1' : ''}
ORDER BY $orderBy ORDER BY $orderBy
'''); ''');
return rows.map(Product.fromMap).toList(); return rows.map(Product.fromMap).toList();
@@ -1436,6 +1438,21 @@ class ProductRepository extends ChangeNotifier {
return rows.map(Supermarket.fromMap).toList(); return rows.map(Supermarket.fromMap).toList();
} }
/// Store names from newest trip to oldest, unique.
Future<List<String>> 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<Supermarket> addSupermarket({ Future<Supermarket> addSupermarket({
required String name, required String name,
required int colorValue, required int colorValue,
+19
View File
@@ -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 { test('deleting all products leaves shopping trips in place', () async {
final repository = memoryRepository(); final repository = memoryRepository();
await repository.saveShop( await repository.saveShop(
+29
View File
@@ -1,4 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/supermarket.dart';
import 'package:receipity/utils/supermarkets.dart'; import 'package:receipity/utils/supermarkets.dart';
void main() { void main() {
@@ -20,4 +21,32 @@ void main() {
test('returns null when no supermarket is present', () { test('returns null when no supermarket is present', () {
expect(detectSupermarket('Milk 1.50\nBread 2.00'), isNull); 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',
]);
});
} }