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
92
apps/app_seeds/lib/data/export_import/seed_label_codec.dart
Normal file
92
apps/app_seeds/lib/data/export_import/seed_label_codec.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// The seed facts a printed label carries in its QR code — just enough to
|
||||
/// identify the seed offline, so the data travels on the physical packet with
|
||||
/// no server and no account. Kept deliberately small so the QR stays scannable
|
||||
/// at sticker size.
|
||||
class SeedLabelData extends Equatable {
|
||||
const SeedLabelData({
|
||||
required this.varietyLabel,
|
||||
this.scientificName,
|
||||
this.commonName,
|
||||
this.year,
|
||||
this.origin,
|
||||
});
|
||||
|
||||
/// The variety's label, as its keeper writes it (e.g. "Acelga de Perales").
|
||||
final String varietyLabel;
|
||||
|
||||
/// Scientific name of the linked species, if any (e.g. "Beta vulgaris").
|
||||
final String? scientificName;
|
||||
|
||||
/// The species' common name in the keeper's locale (e.g. "Acelga").
|
||||
final String? commonName;
|
||||
|
||||
/// Harvest year of the batch, if known.
|
||||
final int? year;
|
||||
|
||||
/// Where the seed came from (origin name and/or place), if recorded.
|
||||
final String? origin;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
varietyLabel,
|
||||
scientificName,
|
||||
commonName,
|
||||
year,
|
||||
origin,
|
||||
];
|
||||
}
|
||||
|
||||
/// Encodes/decodes [SeedLabelData] as a compact, versioned `tanemaki://seed`
|
||||
/// URI — self-describing and parseable, so a future scan-to-import feature can
|
||||
/// read a label a keeper printed today. Unicode and RTL text are percent-encoded
|
||||
/// by [Uri], so any QR reader round-trips them safely.
|
||||
///
|
||||
/// Shape: `tanemaki://seed?v=<variety>&s=<scientific>&c=<common>&y=<year>&o=<origin>`
|
||||
/// Absent fields are omitted, keeping the payload (and the QR) as small as the
|
||||
/// data allows.
|
||||
abstract final class SeedLabelCodec {
|
||||
static const _scheme = 'tanemaki';
|
||||
static const _host = 'seed';
|
||||
|
||||
static String encode(SeedLabelData data) {
|
||||
final params = <String, String>{
|
||||
'v': data.varietyLabel,
|
||||
if (_present(data.scientificName)) 's': data.scientificName!.trim(),
|
||||
if (_present(data.commonName)) 'c': data.commonName!.trim(),
|
||||
if (data.year != null) 'y': '${data.year}',
|
||||
if (_present(data.origin)) 'o': data.origin!.trim(),
|
||||
};
|
||||
return Uri(
|
||||
scheme: _scheme,
|
||||
host: _host,
|
||||
queryParameters: params,
|
||||
).toString();
|
||||
}
|
||||
|
||||
/// Parses a `tanemaki://seed` URI back to [SeedLabelData]. Returns null when
|
||||
/// [input] is not a well-formed seed label (wrong scheme/host, or no variety).
|
||||
static SeedLabelData? decode(String input) {
|
||||
final uri = Uri.tryParse(input.trim());
|
||||
if (uri == null || uri.scheme != _scheme || uri.host != _host) return null;
|
||||
final q = uri.queryParameters;
|
||||
final variety = q['v']?.trim();
|
||||
if (variety == null || variety.isEmpty) return null;
|
||||
final year = q['y'];
|
||||
return SeedLabelData(
|
||||
varietyLabel: variety,
|
||||
scientificName: _clean(q['s']),
|
||||
commonName: _clean(q['c']),
|
||||
year: year == null ? null : int.tryParse(year),
|
||||
origin: _clean(q['o']),
|
||||
);
|
||||
}
|
||||
|
||||
static bool _present(String? value) => value != null && value.trim().isNotEmpty;
|
||||
|
||||
static String? _clean(String? value) {
|
||||
final trimmed = value?.trim();
|
||||
return (trimmed == null || trimmed.isEmpty) ? null : trimmed;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue