feat(labels): print seed labels with QR from an inventory selection

Add multi-select to the inventory and a "Print labels" flow that renders a
PDF sheet of labels (small stickers or large cards), each carrying the seed's
common name, variety, scientific name, year/origin and a tanemaki://seed QR
that holds the seed's data offline (future scan-to-import).

- SeedLabelCodec: compact, versioned tanemaki://seed URI (encode/decode)
- LabelSheetService: pure-Dart PDF grid (pdf + barcode + DejaVu), RTL-aware
- VarietyRepository.labelRows: one row per lot, locale-picked common name
- InventoryCubit selection mode; contextual app bar + label print sheet
- i18n en/es/pt (printLabels.*)

Tests: codec, PDF service (both formats + RTL), labelRows, selection cubit,
inventory widget selection.
This commit is contained in:
vjrj 2026-07-10 19:14:04 +02:00
parent 432acded29
commit efb369eb81
19 changed files with 1465 additions and 116 deletions

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/species_repository.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
@ -85,4 +86,51 @@ void main() {
expect(stamps.toSet().length, 2);
expect(() => stamps.map(Hlc.parse).toList(), returnsNormally);
});
group('labelRows', () {
test('expands one entry per lot and resolves the locale common name', () async {
final speciesRepo = newTestSpeciesRepository(db);
await speciesRepo.seedBundled(const [
SpeciesSeed(
scientificName: 'Beta vulgaris',
family: 'Amaranthaceae',
commonNames: {
'en': ['Chard'],
'es': ['Acelga'],
},
),
]);
final speciesId = (await db.select(db.species).getSingle()).id;
final v = await repo.addQuickVariety(
label: 'Acelga de Perales',
category: 'Amaranthaceae',
);
await repo.linkSpecies(v, speciesId);
await repo.addLot(varietyId: v, harvestYear: 2006, originName: 'Perales');
await repo.addLot(varietyId: v, harvestYear: 2008);
final rows = await repo.labelRows({v}, languageCode: 'es');
expect(rows.length, 2); // one per lot
expect(rows.first.commonName, 'Acelga'); // locale-picked, not 'Chard'
expect(rows.first.scientificName, 'Beta vulgaris');
expect(rows.map((r) => r.harvestYear), [2006, 2008]);
expect(rows.first.originName, 'Perales');
});
test('yields a single name-only entry for a variety with no lots', () async {
final v = await repo.addQuickVariety(label: 'Alubia de Tolosa');
final rows = await repo.labelRows({v}, languageCode: 'en');
expect(rows.single.varietyLabel, 'Alubia de Tolosa');
expect(rows.single.harvestYear, isNull);
expect(rows.single.commonName, isNull);
});
test('includes only varieties in the selection', () async {
final selected = await repo.addQuickVariety(label: 'Selected');
await repo.addQuickVariety(label: 'Not selected');
final rows = await repo.labelRows({selected}, languageCode: 'en');
expect(rows.map((r) => r.varietyLabel), ['Selected']);
});
});
}