feat(plantare): surface product link + dates; spec the bilateral record

The Plantaré screen stored madeOn/dueBy/varietyId but showed none of them,
so it felt half-built. Surface what's already there (no schema change):

- watchPlantareViews() joins each commitment with its variety label
- tile shows the made-on date, a "Return by {date}" line (amber + "overdue"
  when an open promise is past due, via new seedWarning colour), the variety
  name, and taps through to /variety/:id
- add sheet gains an optional return-by date picker (createPlantare already
  took dueBy)
- variety detail shows a read-only commitments section (hidden when empty,
  no add button — honours the single hand-over door)
- i18n en/es/pt/ast; tests for the join, dueBy round-trip, tile, and section

Also specs the deferred two-sided record in docs/design/plantare-bilateral.md
(Nostr pubkey counterparty, PlantareTransport propose->accept->counter-sign,
deferred key/signature/movement columns, provenance DAG, reputation anchor),
with a faithful transcription of the paper Plantaré v0.4 (BAH-Semillero 2009,
CC-BY-SA). Cross-linked from data-model §2.7 and sharing-model §6.
This commit is contained in:
vjrj 2026-07-13 18:06:33 +02:00
parent e57763d0a2
commit adf396d49d
21 changed files with 687 additions and 27 deletions

View file

@ -11,6 +11,10 @@ import 'export_import/import_reconciler.dart';
import 'export_import/inventory_csv_codec.dart';
import 'export_import/inventory_snapshot.dart';
/// A commitment plus the label of the variety it's linked to (if any), for the
/// Plantares list lets a tile name the seed and link back to its detail.
typedef PlantareView = ({Plantare plantare, String? varietyLabel});
/// A lightweight row for the inventory list (only what the list renders).
class VarietyListItem extends Equatable {
const VarietyListItem({
@ -1893,6 +1897,30 @@ class VarietyRepository {
..orderBy([(p) => OrderingTerm.desc(p.madeOn)]))
.watch();
/// Live commitments joined with the label of the variety each is linked to
/// (null when unlinked or the variety is gone), newest first so the list
/// can name the seed and offer a tap-through to its detail.
Stream<List<PlantareView>> watchPlantareViews() {
final query = _db.select(_db.plantares).join([
leftOuterJoin(
_db.varieties,
_db.varieties.id.equalsExp(_db.plantares.varietyId),
),
])
..where(_db.plantares.isDeleted.equals(false))
..orderBy([OrderingTerm.desc(_db.plantares.madeOn)]);
return query.watch().map((rows) => [
for (final row in rows)
(
plantare: row.readTable(_db.plantares),
varietyLabel: switch (row.readTableOrNull(_db.varieties)) {
final v? when !v.isDeleted => v.label,
_ => null,
},
),
]);
}
/// The commitments recorded against one variety.
Stream<List<Plantare>> watchPlantaresForVariety(String varietyId) =>
(_db.select(_db.plantares)