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:
parent
0cecb943f0
commit
bf091bf852
19 changed files with 1465 additions and 116 deletions
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/export_import/seed_label_codec.dart';
|
||||
|
||||
void main() {
|
||||
group('SeedLabelCodec.encode', () {
|
||||
test('encodes all fields as a tanemaki://seed URI', () {
|
||||
final uri = SeedLabelCodec.encode(
|
||||
const SeedLabelData(
|
||||
varietyLabel: 'Acelga de Perales',
|
||||
scientificName: 'Beta vulgaris',
|
||||
commonName: 'Acelga',
|
||||
year: 2006,
|
||||
origin: 'Perales',
|
||||
),
|
||||
);
|
||||
expect(uri, startsWith('tanemaki://seed?'));
|
||||
final parsed = Uri.parse(uri);
|
||||
expect(parsed.scheme, 'tanemaki');
|
||||
expect(parsed.host, 'seed');
|
||||
expect(parsed.queryParameters, {
|
||||
'v': 'Acelga de Perales',
|
||||
's': 'Beta vulgaris',
|
||||
'c': 'Acelga',
|
||||
'y': '2006',
|
||||
'o': 'Perales',
|
||||
});
|
||||
});
|
||||
|
||||
test('omits absent fields, keeping the payload small', () {
|
||||
final uri = SeedLabelCodec.encode(
|
||||
const SeedLabelData(varietyLabel: 'Alubia de Tolosa'),
|
||||
);
|
||||
final parsed = Uri.parse(uri);
|
||||
expect(parsed.queryParameters, {'v': 'Alubia de Tolosa'});
|
||||
expect(uri, isNot(contains('&s=')));
|
||||
expect(uri, isNot(contains('y=')));
|
||||
});
|
||||
|
||||
test('treats blank optional fields as absent', () {
|
||||
final uri = SeedLabelCodec.encode(
|
||||
const SeedLabelData(
|
||||
varietyLabel: 'Maíz',
|
||||
scientificName: ' ',
|
||||
origin: '',
|
||||
),
|
||||
);
|
||||
expect(Uri.parse(uri).queryParameters, {'v': 'Maíz'});
|
||||
});
|
||||
});
|
||||
|
||||
group('SeedLabelCodec round-trip', () {
|
||||
test('decode reverses encode for full data', () {
|
||||
const data = SeedLabelData(
|
||||
varietyLabel: 'Tomate Rosa',
|
||||
scientificName: 'Solanum lycopersicum',
|
||||
commonName: 'Tomate',
|
||||
year: 2024,
|
||||
origin: 'Huerta de la abuela, León',
|
||||
);
|
||||
expect(SeedLabelCodec.decode(SeedLabelCodec.encode(data)), data);
|
||||
});
|
||||
|
||||
test('survives unicode, spaces and separators', () {
|
||||
const data = SeedLabelData(
|
||||
varietyLabel: 'Grelo & nabo "do país"',
|
||||
origin: 'A Coruña / Galiza',
|
||||
commonName: 'بامية', // RTL script
|
||||
);
|
||||
final decoded = SeedLabelCodec.decode(SeedLabelCodec.encode(data));
|
||||
expect(decoded, data);
|
||||
});
|
||||
});
|
||||
|
||||
group('SeedLabelCodec.decode', () {
|
||||
test('rejects a foreign scheme', () {
|
||||
expect(SeedLabelCodec.decode('https://seed?v=x'), isNull);
|
||||
expect(SeedLabelCodec.decode('tanemaki://other?v=x'), isNull);
|
||||
});
|
||||
|
||||
test('rejects a payload without a variety', () {
|
||||
expect(SeedLabelCodec.decode('tanemaki://seed?s=Beta%20vulgaris'), isNull);
|
||||
});
|
||||
|
||||
test('ignores an unparseable year rather than throwing', () {
|
||||
final decoded = SeedLabelCodec.decode('tanemaki://seed?v=Maíz&y=old');
|
||||
expect(decoded?.varietyLabel, 'Maíz');
|
||||
expect(decoded?.year, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue