feat(app): show each batch's story as a composite timeline

The Movement log (history + provenance DAG) was recorded but invisible.
A new history sheet on every lot tile narrates the batch: movements,
germination tests, storage checks and how it entered the collection
(origin attached), most-recent first. Two one-tap chips record 'sown
today' / 'harvested today' — the first UI door to the sown/harvested
movement types. No schema change; one-shot reads only (no live
subscriptions in a transient sheet).
This commit is contained in:
vjrj 2026-07-18 06:25:55 +02:00
parent 11cbdf3022
commit 92fd84590b
10 changed files with 835 additions and 2 deletions

View file

@ -700,6 +700,25 @@
"promiseGave": "They'll return me seed",
"promiseReceived": "I'll return seed"
},
"history": {
"title": "Story of this batch",
"tooltip": "Story",
"sowToday": "Sown today",
"harvestToday": "Harvested today",
"sownRecorded": "Sowing noted",
"harvestRecorded": "Harvest noted",
"movementReceived": "Received",
"movementGiven": "Given away",
"movementSown": "Sown",
"movementHarvested": "Harvested",
"movementGerminationTest": "Germination test",
"movementSplit": "Split into batches",
"movementDiscarded": "Discarded",
"created": "Added to your collection",
"from": "From {origin}",
"germinationResult": "Germination test — {percent}%",
"linkedEarlier": "Comes from an earlier batch"
},
"sale": {
"title": "Sales",
"help": "Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.",

View file

@ -699,6 +699,25 @@
"promiseGave": "Me devolverán semilla",
"promiseReceived": "Devolveré semilla"
},
"history": {
"title": "Historia de este lote",
"tooltip": "Historia",
"sowToday": "Sembrado hoy",
"harvestToday": "Cosechado hoy",
"sownRecorded": "Siembra anotada",
"harvestRecorded": "Cosecha anotada",
"movementReceived": "Recibido",
"movementGiven": "Regalado",
"movementSown": "Sembrado",
"movementHarvested": "Cosechado",
"movementGerminationTest": "Prueba de germinación",
"movementSplit": "Repartido en lotes",
"movementDiscarded": "Descartado",
"created": "Entró en tu colección",
"from": "De {origin}",
"germinationResult": "Prueba de germinación — {percent}%",
"linkedEarlier": "Viene de un lote anterior"
},
"sale": {
"title": "Ventas",
"help": "Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 7
/// Strings: 3494 (499 per locale)
/// Strings: 3528 (504 per locale)
///
/// Built on 2026-07-17 at 11:10 UTC
/// Built on 2026-07-18 at 04:22 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View file

@ -86,6 +86,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final Translations$notifications$en notifications = Translations$notifications$en.internal(_root);
late final Translations$plantare$en plantare = Translations$plantare$en.internal(_root);
late final Translations$handover$en handover = Translations$handover$en.internal(_root);
late final Translations$history$en history = Translations$history$en.internal(_root);
late final Translations$sale$en sale = Translations$sale$en.internal(_root);
late final Translations$legal$en legal = Translations$legal$en.internal(_root);
late final Translations$marketGate$en marketGate = Translations$marketGate$en.internal(_root);
@ -1980,6 +1981,66 @@ class Translations$handover$en {
String get promiseReceived => 'I\'ll return seed';
}
// Path: history
class Translations$history$en {
Translations$history$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Story of this batch'
String get title => 'Story of this batch';
/// en: 'Story'
String get tooltip => 'Story';
/// en: 'Sown today'
String get sowToday => 'Sown today';
/// en: 'Harvested today'
String get harvestToday => 'Harvested today';
/// en: 'Sowing noted'
String get sownRecorded => 'Sowing noted';
/// en: 'Harvest noted'
String get harvestRecorded => 'Harvest noted';
/// en: 'Received'
String get movementReceived => 'Received';
/// en: 'Given away'
String get movementGiven => 'Given away';
/// en: 'Sown'
String get movementSown => 'Sown';
/// en: 'Harvested'
String get movementHarvested => 'Harvested';
/// en: 'Germination test'
String get movementGerminationTest => 'Germination test';
/// en: 'Split into batches'
String get movementSplit => 'Split into batches';
/// en: 'Discarded'
String get movementDiscarded => 'Discarded';
/// en: 'Added to your collection'
String get created => 'Added to your collection';
/// en: 'From {origin}'
String from({required Object origin}) => 'From ${origin}';
/// en: 'Germination test — {percent}%'
String germinationResult({required Object percent}) => 'Germination test — ${percent}%';
/// en: 'Comes from an earlier batch'
String get linkedEarlier => 'Comes from an earlier batch';
}
// Path: sale
class Translations$sale$en {
Translations$sale$en.internal(this._root);
@ -3199,6 +3260,23 @@ extension on Translations {
'handover.paymentChip' => 'Money changed hands',
'handover.promiseGave' => 'They\'ll return me seed',
'handover.promiseReceived' => 'I\'ll return seed',
'history.title' => 'Story of this batch',
'history.tooltip' => 'Story',
'history.sowToday' => 'Sown today',
'history.harvestToday' => 'Harvested today',
'history.sownRecorded' => 'Sowing noted',
'history.harvestRecorded' => 'Harvest noted',
'history.movementReceived' => 'Received',
'history.movementGiven' => 'Given away',
'history.movementSown' => 'Sown',
'history.movementHarvested' => 'Harvested',
'history.movementGerminationTest' => 'Germination test',
'history.movementSplit' => 'Split into batches',
'history.movementDiscarded' => 'Discarded',
'history.created' => 'Added to your collection',
'history.from' => ({required Object origin}) => 'From ${origin}',
'history.germinationResult' => ({required Object percent}) => 'Germination test — ${percent}%',
'history.linkedEarlier' => 'Comes from an earlier batch',
'sale.title' => 'Sales',
'sale.help' => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.',
'sale.add' => 'Record a sale',

View file

@ -85,6 +85,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$notifications$es notifications = _Translations$notifications$es._(_root);
@override late final _Translations$plantare$es plantare = _Translations$plantare$es._(_root);
@override late final _Translations$handover$es handover = _Translations$handover$es._(_root);
@override late final _Translations$history$es history = _Translations$history$es._(_root);
@override late final _Translations$sale$es sale = _Translations$sale$es._(_root);
@override late final _Translations$legal$es legal = _Translations$legal$es._(_root);
@override late final _Translations$marketGate$es marketGate = _Translations$marketGate$es._(_root);
@ -1021,6 +1022,32 @@ class _Translations$handover$es extends Translations$handover$en {
@override String get promiseReceived => 'Devolveré semilla';
}
// Path: history
class _Translations$history$es extends Translations$history$en {
_Translations$history$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Historia de este lote';
@override String get tooltip => 'Historia';
@override String get sowToday => 'Sembrado hoy';
@override String get harvestToday => 'Cosechado hoy';
@override String get sownRecorded => 'Siembra anotada';
@override String get harvestRecorded => 'Cosecha anotada';
@override String get movementReceived => 'Recibido';
@override String get movementGiven => 'Regalado';
@override String get movementSown => 'Sembrado';
@override String get movementHarvested => 'Cosechado';
@override String get movementGerminationTest => 'Prueba de germinación';
@override String get movementSplit => 'Repartido en lotes';
@override String get movementDiscarded => 'Descartado';
@override String get created => 'Entró en tu colección';
@override String from({required Object origin}) => 'De ${origin}';
@override String germinationResult({required Object percent}) => 'Prueba de germinación — ${percent}%';
@override String get linkedEarlier => 'Viene de un lote anterior';
}
// Path: sale
class _Translations$sale$es extends Translations$sale$en {
_Translations$sale$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -2011,6 +2038,23 @@ extension on TranslationsEs {
'handover.paymentChip' => 'Hubo dinero por medio',
'handover.promiseGave' => 'Me devolverán semilla',
'handover.promiseReceived' => 'Devolveré semilla',
'history.title' => 'Historia de este lote',
'history.tooltip' => 'Historia',
'history.sowToday' => 'Sembrado hoy',
'history.harvestToday' => 'Cosechado hoy',
'history.sownRecorded' => 'Siembra anotada',
'history.harvestRecorded' => 'Cosecha anotada',
'history.movementReceived' => 'Recibido',
'history.movementGiven' => 'Regalado',
'history.movementSown' => 'Sembrado',
'history.movementHarvested' => 'Cosechado',
'history.movementGerminationTest' => 'Prueba de germinación',
'history.movementSplit' => 'Repartido en lotes',
'history.movementDiscarded' => 'Descartado',
'history.created' => 'Entró en tu colección',
'history.from' => ({required Object origin}) => 'De ${origin}',
'history.germinationResult' => ({required Object percent}) => 'Prueba de germinación — ${percent}%',
'history.linkedEarlier' => 'Viene de un lote anterior',
'sale.title' => 'Ventas',
'sale.help' => 'Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.',
'sale.add' => 'Registrar venta',