feat(app): scan a printed seed label back into its record
Closes the physical↔digital loop the labels opened: the QR on a printed envelope (tane://seed, already encoded by seed_label_codec) can now be scanned from the inventory bar. A known label opens its variety; an unknown one asks before adding anything, then creates the variety (species linked by exact scientific name when bundled) with a lot carrying the label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner, MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships. Mobile-only; other platforms don't show the button (OCR precedent). The post-scan flow is a plain function (handleScannedPayload) so it's widget-tested without a camera.
This commit is contained in:
parent
bb5b3d4a43
commit
f1aa8f8e66
18 changed files with 1130 additions and 42 deletions
66
apps/app_seeds/lib/services/seed_label_scan.dart
Normal file
66
apps/app_seeds/lib/services/seed_label_scan.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../data/export_import/seed_label_codec.dart';
|
||||
import '../data/species_repository.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
|
||||
/// What scanning a QR yielded: not a seed label at all, a variety already in
|
||||
/// the collection ([varietyId] set), or a seed not held yet ([data] set — the
|
||||
/// UI asks before adding anything).
|
||||
class SeedScanResult extends Equatable {
|
||||
const SeedScanResult.notALabel() : varietyId = null, data = null;
|
||||
const SeedScanResult.match(String this.varietyId) : data = null;
|
||||
const SeedScanResult.unknown(SeedLabelData this.data) : varietyId = null;
|
||||
|
||||
final String? varietyId;
|
||||
final SeedLabelData? data;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [varietyId, data];
|
||||
}
|
||||
|
||||
/// Resolves a scanned QR payload against the collection: decodes the
|
||||
/// `tane://seed` label and looks the variety up by its label (the identity a
|
||||
/// keeper printed on the envelope). This is how a physical packet finds its
|
||||
/// way back to its record — even a re-saved one whose digital thread broke.
|
||||
Future<SeedScanResult> resolveScannedLabel(
|
||||
VarietyRepository repository,
|
||||
String raw,
|
||||
) async {
|
||||
final data = SeedLabelCodec.decode(raw);
|
||||
if (data == null) return const SeedScanResult.notALabel();
|
||||
final varietyId = await repository.findVarietyIdByLabel(data.varietyLabel);
|
||||
return varietyId == null
|
||||
? SeedScanResult.unknown(data)
|
||||
: SeedScanResult.match(varietyId);
|
||||
}
|
||||
|
||||
/// Adds the scanned seed to the collection — a variety named as the label
|
||||
/// says, linked to the bundled species when the scientific name matches
|
||||
/// exactly, with one lot carrying the label's harvest year and origin.
|
||||
/// Called only after the person confirmed the add prompt.
|
||||
Future<String> importScannedLabel({
|
||||
required VarietyRepository repository,
|
||||
required SpeciesRepository species,
|
||||
required SeedLabelData data,
|
||||
}) async {
|
||||
final varietyId = await repository.addQuickVariety(label: data.varietyLabel);
|
||||
|
||||
final scientific = data.scientificName?.trim();
|
||||
if (scientific != null && scientific.isNotEmpty) {
|
||||
final matches = await species.search(scientific, limit: 4);
|
||||
for (final m in matches) {
|
||||
if (m.scientificName.toLowerCase() == scientific.toLowerCase()) {
|
||||
await repository.linkSpecies(varietyId, m.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await repository.addLot(
|
||||
varietyId: varietyId,
|
||||
harvestYear: data.year,
|
||||
originName: data.origin,
|
||||
);
|
||||
return varietyId;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue