feat(inventory): filter by category and lot form + a11y polish
Add filter chips above the inventory list, one per category in use and one per lot form actually held; results apply search ∧ category ∧ form. An active filter that empties the list shows a distinct 'no matches' state and a clear-filters button. VarietyListItem now carries the set of lot forms it holds (one aggregate query), and the stream re-emits on lot changes. Accessibility: category headers use the text theme so they honour the system font scale; the list avatar is excluded from semantics (the tile title already announces the name); the edit action uses the green action colour (≥3:1 on the canvas, up from 2.62:1). Add category/form/clear filter tests and a tap-target-size guideline test.
This commit is contained in:
parent
42c16c0e3f
commit
9558115c1e
9 changed files with 337 additions and 24 deletions
|
|
@ -14,6 +14,7 @@ class VarietyListItem extends Equatable {
|
|||
this.category,
|
||||
this.scientificName,
|
||||
this.photo,
|
||||
this.lotTypes = const {},
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
|
@ -26,8 +27,19 @@ class VarietyListItem extends Equatable {
|
|||
/// First photo (encrypted BLOB) for the avatar, or null → show an initial.
|
||||
final Uint8List? photo;
|
||||
|
||||
/// Distinct lot forms this variety currently holds (seed, plant, tree…),
|
||||
/// used to filter the list by form. Empty when it has no lots yet.
|
||||
final Set<LotType> lotTypes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, label, category, scientificName, photo];
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
label,
|
||||
category,
|
||||
scientificName,
|
||||
photo,
|
||||
lotTypes,
|
||||
];
|
||||
}
|
||||
|
||||
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
|
||||
|
|
@ -221,6 +233,8 @@ class VarietyRepository {
|
|||
)..where((v) => v.isDeleted.equals(false))).watch().map((_) {}),
|
||||
_db.select(_db.attachments).watch().map((_) {}),
|
||||
_db.select(_db.species).watch().map((_) {}),
|
||||
// Lots drive the form filter, so re-emit when they change too.
|
||||
_db.select(_db.lots).watch().map((_) {}),
|
||||
]);
|
||||
return triggers.asyncMap((_) => _loadInventory());
|
||||
}
|
||||
|
|
@ -234,10 +248,12 @@ class VarietyRepository {
|
|||
(v) => OrderingTerm(expression: v.label),
|
||||
]))
|
||||
.get();
|
||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||
final varietyIds = rows.map((v) => v.id).toList();
|
||||
final photos = await _firstPhotosFor(varietyIds);
|
||||
final sciNames = await _scientificNamesFor(
|
||||
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
||||
);
|
||||
final lotTypes = await _lotTypesFor(varietyIds);
|
||||
return rows
|
||||
.map(
|
||||
(v) => VarietyListItem(
|
||||
|
|
@ -246,11 +262,30 @@ class VarietyRepository {
|
|||
category: v.category,
|
||||
scientificName: v.speciesId == null ? null : sciNames[v.speciesId],
|
||||
photo: photos[v.id],
|
||||
lotTypes: lotTypes[v.id] ?? const {},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Maps each variety to the distinct lot forms it currently holds (one
|
||||
/// query). Varieties without lots are simply absent from the map.
|
||||
Future<Map<String, Set<LotType>>> _lotTypesFor(
|
||||
List<String> varietyIds,
|
||||
) async {
|
||||
if (varietyIds.isEmpty) return const {};
|
||||
final rows =
|
||||
await (_db.select(_db.lots)..where(
|
||||
(l) => l.varietyId.isIn(varietyIds) & l.isDeleted.equals(false),
|
||||
))
|
||||
.get();
|
||||
final byVariety = <String, Set<LotType>>{};
|
||||
for (final row in rows) {
|
||||
(byVariety[row.varietyId] ??= <LotType>{}).add(row.type);
|
||||
}
|
||||
return byVariety;
|
||||
}
|
||||
|
||||
/// Loads the first photo BLOB for each of [varietyIds] (one query).
|
||||
Future<Map<String, Uint8List>> _firstPhotosFor(
|
||||
List<String> varietyIds,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue