feat(app): ask 'how did it do?' when a harvest is noted (schema v13)
The note growers most wish they had two seasons later — did it do well, was it worth keeping — never had a home. New append-only GardenOutcomes table (lotId, season year, three-face rating, free note); the question appears ONCE, inline, right after recording a harvest in the batch story, and is skippable without a trace. The answer shows as one more story line. Rides backups/sync like every mutable table (JSON codec, LWW import, HLC clock absorb). Migration v12→v13 guarded + verified from every historical version.
This commit is contained in:
parent
92fd84590b
commit
bb5b3d4a43
20 changed files with 6030 additions and 15 deletions
|
|
@ -153,6 +153,23 @@ class InventoryJsonCodec {
|
|||
'notes': c.notes,
|
||||
},
|
||||
],
|
||||
'gardenOutcomes': [
|
||||
for (final o in snapshot.gardenOutcomes)
|
||||
{
|
||||
..._syncMeta(
|
||||
id: o.id,
|
||||
createdAt: o.createdAt,
|
||||
updatedAt: o.updatedAt,
|
||||
lastAuthor: o.lastAuthor,
|
||||
schemaRowVersion: o.schemaRowVersion,
|
||||
isDeleted: o.isDeleted,
|
||||
),
|
||||
'lotId': o.lotId,
|
||||
'year': o.year,
|
||||
'rating': o.rating?.name,
|
||||
'notes': o.notes,
|
||||
},
|
||||
],
|
||||
'movements': [
|
||||
for (final m in snapshot.movements)
|
||||
{
|
||||
|
|
@ -417,6 +434,20 @@ class InventoryJsonCodec {
|
|||
notes: m['notes'] as String?,
|
||||
);
|
||||
}),
|
||||
gardenOutcomes: _rows(root, 'gardenOutcomes', (m) {
|
||||
return GardenOutcome(
|
||||
id: _string(m, 'id'),
|
||||
createdAt: _int(m, 'createdAt'),
|
||||
updatedAt: _string(m, 'updatedAt'),
|
||||
lastAuthor: _string(m, 'lastAuthor'),
|
||||
isDeleted: _bool(m, 'isDeleted'),
|
||||
schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
|
||||
lotId: _string(m, 'lotId'),
|
||||
year: m['year'] as int?,
|
||||
rating: _enumOrNull(GardenOutcomeRating.values, m['rating']),
|
||||
notes: m['notes'] as String?,
|
||||
);
|
||||
}),
|
||||
movements: _rows(root, 'movements', (m) {
|
||||
final type = _enumOrNull(MovementType.values, m['type']);
|
||||
if (type == null) return null; // no safe default → drop row
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class InventorySnapshot {
|
|||
this.externalLinks = const [],
|
||||
this.germinationTests = const [],
|
||||
this.conditionChecks = const [],
|
||||
this.gardenOutcomes = const [],
|
||||
this.movements = const [],
|
||||
this.parties = const [],
|
||||
this.attachments = const [],
|
||||
|
|
@ -41,6 +42,7 @@ class InventorySnapshot {
|
|||
final List<ExternalLink> externalLinks;
|
||||
final List<GerminationTest> germinationTests;
|
||||
final List<ConditionCheck> conditionChecks;
|
||||
final List<GardenOutcome> gardenOutcomes;
|
||||
final List<Movement> movements;
|
||||
final List<Party> parties;
|
||||
final List<Attachment> attachments;
|
||||
|
|
|
|||
|
|
@ -327,7 +327,13 @@ class ConditionEntry extends Equatable {
|
|||
}
|
||||
|
||||
/// What kind of event a [LotHistoryEntry] narrates.
|
||||
enum LotHistoryKind { created, movement, germinationTest, conditionCheck }
|
||||
enum LotHistoryKind {
|
||||
created,
|
||||
movement,
|
||||
germinationTest,
|
||||
conditionCheck,
|
||||
gardenOutcome,
|
||||
}
|
||||
|
||||
/// One line of a lot's composite history — the batch's own story, merged from
|
||||
/// what is already recorded (the lot's creation with its origin, movements,
|
||||
|
|
@ -346,6 +352,8 @@ class LotHistoryEntry extends Equatable {
|
|||
this.germinationRate,
|
||||
this.containerCount,
|
||||
this.desiccantState,
|
||||
this.rating,
|
||||
this.year,
|
||||
this.hasParentLink = false,
|
||||
});
|
||||
|
||||
|
|
@ -374,6 +382,10 @@ class LotHistoryEntry extends Equatable {
|
|||
final int? containerCount;
|
||||
final DesiccantState? desiccantState;
|
||||
|
||||
/// Set for [LotHistoryKind.gardenOutcome] entries: how that season went.
|
||||
final GardenOutcomeRating? rating;
|
||||
final int? year;
|
||||
|
||||
/// True when the movement links back to a source batch (provenance DAG).
|
||||
final bool hasParentLink;
|
||||
|
||||
|
|
@ -390,6 +402,8 @@ class LotHistoryEntry extends Equatable {
|
|||
germinationRate,
|
||||
containerCount,
|
||||
desiccantState,
|
||||
rating,
|
||||
year,
|
||||
hasParentLink,
|
||||
];
|
||||
}
|
||||
|
|
@ -1973,6 +1987,33 @@ class VarietyRepository {
|
|||
return id;
|
||||
}
|
||||
|
||||
/// Records how a season went for a lot — the optional, skippable answer to
|
||||
/// "how did it do?" asked when a harvest is noted. Returns the new row id.
|
||||
Future<String> addGardenOutcome({
|
||||
required String lotId,
|
||||
int? year,
|
||||
GardenOutcomeRating? rating,
|
||||
String? notes,
|
||||
}) async {
|
||||
final (created, updated) = await _stamp();
|
||||
final id = idGen.newId();
|
||||
await _db
|
||||
.into(_db.gardenOutcomes)
|
||||
.insert(
|
||||
GardenOutcomesCompanion.insert(
|
||||
id: id,
|
||||
lotId: lotId,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
year: Value(year),
|
||||
rating: Value(rating),
|
||||
notes: Value(_hasText(notes) ? notes!.trim() : null),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// The lot's composite history, most-recent first: every movement,
|
||||
/// germination test and condition check already recorded, closed by the
|
||||
/// lot's own creation entry (carrying its origin, so "received from…" shows
|
||||
|
|
@ -1993,6 +2034,9 @@ class VarietyRepository {
|
|||
final checks = await (_db.select(
|
||||
_db.conditionChecks,
|
||||
)..where((c) => c.lotId.equals(lotId) & c.isDeleted.equals(false))).get();
|
||||
final outcomes = await (_db.select(
|
||||
_db.gardenOutcomes,
|
||||
)..where((o) => o.lotId.equals(lotId) & o.isDeleted.equals(false))).get();
|
||||
|
||||
// Resolve counterparty display names in one go (usually zero or one).
|
||||
final partyIds = movements
|
||||
|
|
@ -2047,6 +2091,14 @@ class VarietyRepository {
|
|||
containerCount: c.containerCount,
|
||||
desiccantState: c.desiccantState,
|
||||
),
|
||||
for (final o in outcomes)
|
||||
LotHistoryEntry(
|
||||
kind: LotHistoryKind.gardenOutcome,
|
||||
when: o.createdAt,
|
||||
notes: o.notes,
|
||||
rating: o.rating,
|
||||
year: o.year,
|
||||
),
|
||||
]..sort((a, b) => (b.when ?? 0).compareTo(a.when ?? 0));
|
||||
|
||||
return [
|
||||
|
|
@ -2451,6 +2503,9 @@ class VarietyRepository {
|
|||
conditionChecks: await (_db.select(
|
||||
_db.conditionChecks,
|
||||
)..where((c) => c.isDeleted.equals(false))).get(),
|
||||
gardenOutcomes: await (_db.select(
|
||||
_db.gardenOutcomes,
|
||||
)..where((o) => o.isDeleted.equals(false))).get(),
|
||||
movements: await _db.select(_db.movements).get(),
|
||||
parties: await (_db.select(
|
||||
_db.parties,
|
||||
|
|
@ -2484,6 +2539,7 @@ class VarietyRepository {
|
|||
externalLinks: await _db.select(_db.externalLinks).get(),
|
||||
germinationTests: await _db.select(_db.germinationTests).get(),
|
||||
conditionChecks: await _db.select(_db.conditionChecks).get(),
|
||||
gardenOutcomes: await _db.select(_db.gardenOutcomes).get(),
|
||||
movements: await _db.select(_db.movements).get(),
|
||||
parties: await _db.select(_db.parties).get(),
|
||||
plantares: await _db.select(_db.plantares).get(),
|
||||
|
|
@ -2564,6 +2620,14 @@ class VarietyRepository {
|
|||
updatedAtOf: (r) => r.updatedAt,
|
||||
reconciler: reconciler,
|
||||
);
|
||||
summary += await _importMutableRows(
|
||||
table: _db.gardenOutcomes,
|
||||
idColumn: _db.gardenOutcomes.id,
|
||||
rows: snapshot.gardenOutcomes,
|
||||
idOf: (r) => r.id,
|
||||
updatedAtOf: (r) => r.updatedAt,
|
||||
reconciler: reconciler,
|
||||
);
|
||||
summary += await _importMutableRows(
|
||||
table: _db.parties,
|
||||
idColumn: _db.parties.id,
|
||||
|
|
@ -2606,6 +2670,7 @@ class VarietyRepository {
|
|||
for (final e in snapshot.externalLinks) e.updatedAt,
|
||||
for (final g in snapshot.germinationTests) g.updatedAt,
|
||||
for (final c in snapshot.conditionChecks) c.updatedAt,
|
||||
for (final o in snapshot.gardenOutcomes) o.updatedAt,
|
||||
for (final p in snapshot.parties) p.updatedAt,
|
||||
for (final a in snapshot.attachments) a.updatedAt,
|
||||
for (final pl in snapshot.plantares) pl.updatedAt,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue