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 0cecb943f0
commit bf091bf852
19 changed files with 1465 additions and 116 deletions

View file

@ -147,4 +147,50 @@ void main() {
expect(state.categories, hasLength(2));
expect(state.categories.toSet(), {'Poaceae', 'Fabaceae'});
});
group('selection mode', () {
test('enterSelection starts the mode with one id', () async {
await repo.addQuickVariety(label: 'Bean');
final state = await waitFor(cubit, (s) => s.items.isNotEmpty);
final id = state.items.single.id;
cubit.enterSelection(id);
expect(cubit.state.selectionMode, isTrue);
expect(cubit.state.selectedIds, {id});
});
test('toggleSelection adds then removes, staying in mode', () async {
await repo.addQuickVariety(label: 'Bean');
final state = await waitFor(cubit, (s) => s.items.isNotEmpty);
final id = state.items.single.id;
cubit.toggleSelection(id);
expect(cubit.state.selectedIds, {id});
cubit.toggleSelection(id);
expect(cubit.state.selectedIds, isEmpty);
expect(cubit.state.selectionMode, isTrue);
});
test('selectAllVisible selects only the filtered subset', () async {
await repo.addQuickVariety(label: 'Bean', category: 'Fabaceae');
await repo.addQuickVariety(label: 'Tomato', category: 'Solanaceae');
final state = await waitFor(cubit, (s) => s.items.length == 2);
final beanId = state.items.firstWhere((i) => i.label == 'Bean').id;
cubit.toggleCategory('Fabaceae'); // hide the tomato
cubit.selectAllVisible();
expect(cubit.state.selectedIds, {beanId});
});
test('exitSelection leaves the mode and clears the selection', () async {
await repo.addQuickVariety(label: 'Bean');
final state = await waitFor(cubit, (s) => s.items.isNotEmpty);
cubit.enterSelection(state.items.single.id);
cubit.exitSelection();
expect(cubit.state.selectionMode, isFalse);
expect(cubit.state.selectedIds, isEmpty);
});
});
}