feat(block1): germination tests per lot with derived rate
Record germination tests on a lot and surface the result. - GerminationEntry model with a derived rate (germinated / sample). VarietyLot carries its tests (most-recent first) and exposes latestGerminationRate. - VarietyRepository.addGerminationTest; watchVariety now also re-emits on germination-test changes, so the detail refreshes live. - Detail UI: each lot shows a germination % badge and a grass action that opens a sheet listing past tests and adding a new one (germinated + sample size). i18n strings (ES/EN). Tests: derived-rate + reactivity at the repository, and a widget test for the record-test → badge flow. Full suite: 35 passing, 0 skipped.
This commit is contained in:
parent
4e8b8293e0
commit
37427fa738
10 changed files with 405 additions and 6 deletions
|
|
@ -18,22 +18,63 @@ class VarietyListItem extends Equatable {
|
|||
List<Object?> get props => [id, label, category];
|
||||
}
|
||||
|
||||
/// One held batch of a variety, for the detail view.
|
||||
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
|
||||
/// be computed.
|
||||
class GerminationEntry extends Equatable {
|
||||
const GerminationEntry({
|
||||
required this.id,
|
||||
this.testedOn,
|
||||
this.sampleSize,
|
||||
this.germinatedCount,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final int? testedOn; // ms since epoch
|
||||
final int? sampleSize;
|
||||
final int? germinatedCount;
|
||||
final String? notes;
|
||||
|
||||
double? get rate {
|
||||
final sample = sampleSize;
|
||||
final germinated = germinatedCount;
|
||||
if (sample == null || sample <= 0 || germinated == null) return null;
|
||||
return germinated / sample;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, testedOn, sampleSize, germinatedCount, notes];
|
||||
}
|
||||
|
||||
/// One held batch of a variety, for the detail view. [germinationTests] are
|
||||
/// ordered most-recent first, so `germinationTests.first` is the latest.
|
||||
class VarietyLot extends Equatable {
|
||||
const VarietyLot({
|
||||
required this.id,
|
||||
this.harvestYear,
|
||||
this.quantity,
|
||||
this.storageLocation,
|
||||
this.germinationTests = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
final int? harvestYear;
|
||||
final Quantity? quantity;
|
||||
final String? storageLocation;
|
||||
final List<GerminationEntry> germinationTests;
|
||||
|
||||
/// The most recent germination rate (0..1), or null if there are no tests.
|
||||
double? get latestGerminationRate =>
|
||||
germinationTests.isEmpty ? null : germinationTests.first.rate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, harvestYear, quantity, storageLocation];
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
harvestYear,
|
||||
quantity,
|
||||
storageLocation,
|
||||
germinationTests,
|
||||
];
|
||||
}
|
||||
|
||||
/// The full detail of one variety (identity + its lots, names and first photo).
|
||||
|
|
@ -197,6 +238,8 @@ class VarietyRepository {
|
|||
(_db.select(
|
||||
_db.attachments,
|
||||
)..where((a) => a.parentId.equals(id))).watch().map((_) {}),
|
||||
// Coarse: any germination-test change re-emits (catalog of tests is tiny).
|
||||
_db.select(_db.germinationTests).watch().map((_) {}),
|
||||
]);
|
||||
return triggers.asyncMap((_) => _loadVariety(id));
|
||||
}
|
||||
|
|
@ -221,6 +264,30 @@ class VarietyRepository {
|
|||
..where((l) => l.varietyId.equals(id) & l.isDeleted.equals(false))
|
||||
..orderBy([(l) => OrderingTerm.desc(l.harvestYear)]))
|
||||
.get();
|
||||
|
||||
final lotIds = lots.map((l) => l.id).toList();
|
||||
final testsByLot = <String, List<GerminationEntry>>{};
|
||||
if (lotIds.isNotEmpty) {
|
||||
final tests =
|
||||
await (_db.select(_db.germinationTests)
|
||||
..where((g) => g.lotId.isIn(lotIds) & g.isDeleted.equals(false))
|
||||
..orderBy([(g) => OrderingTerm.desc(g.testedOn)]))
|
||||
.get();
|
||||
for (final t in tests) {
|
||||
testsByLot
|
||||
.putIfAbsent(t.lotId, () => [])
|
||||
.add(
|
||||
GerminationEntry(
|
||||
id: t.id,
|
||||
testedOn: t.testedOn,
|
||||
sampleSize: t.sampleSize,
|
||||
germinatedCount: t.germinatedCount,
|
||||
notes: t.notes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final names = await (_db.select(
|
||||
_db.varietyVernacularNames,
|
||||
)..where((n) => n.varietyId.equals(id) & n.isDeleted.equals(false))).get();
|
||||
|
|
@ -243,7 +310,7 @@ class VarietyRepository {
|
|||
notes: v.notes,
|
||||
speciesId: v.speciesId,
|
||||
scientificName: scientificName,
|
||||
lots: lots.map(_toLot).toList(),
|
||||
lots: lots.map((l) => _toLot(l, testsByLot[l.id] ?? const [])).toList(),
|
||||
vernacularNames: names.map((n) => n.name).toList(),
|
||||
photo: photos.isEmpty ? null : photos.first.bytes,
|
||||
);
|
||||
|
|
@ -337,7 +404,35 @@ class VarietyRepository {
|
|||
);
|
||||
}
|
||||
|
||||
VarietyLot _toLot(Lot l) {
|
||||
/// Records a germination test for a lot. Returns the new test id.
|
||||
Future<String> addGerminationTest({
|
||||
required String lotId,
|
||||
int? testedOn,
|
||||
int? sampleSize,
|
||||
int? germinatedCount,
|
||||
String? notes,
|
||||
}) async {
|
||||
final (created, updated) = _stamp();
|
||||
final id = idGen.newId();
|
||||
await _db
|
||||
.into(_db.germinationTests)
|
||||
.insert(
|
||||
GerminationTestsCompanion.insert(
|
||||
id: id,
|
||||
lotId: lotId,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
testedOn: Value(testedOn),
|
||||
sampleSize: Value(sampleSize),
|
||||
germinatedCount: Value(germinatedCount),
|
||||
notes: Value(notes),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
VarietyLot _toLot(Lot l, List<GerminationEntry> germinationTests) {
|
||||
final hasQuantity =
|
||||
l.quantityKind != null ||
|
||||
l.quantityPrecise != null ||
|
||||
|
|
@ -346,6 +441,7 @@ class VarietyRepository {
|
|||
id: l.id,
|
||||
harvestYear: l.harvestYear,
|
||||
storageLocation: l.storageLocation,
|
||||
germinationTests: germinationTests,
|
||||
quantity: hasQuantity
|
||||
? Quantity(
|
||||
kind: _parseKind(l.quantityKind),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue