fixes before build
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -72,4 +72,4 @@ class Product {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProductSort { name, preferred, start }
|
enum ProductSort { name, preferred, favourite }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user