feat(inventory): photo-first drafts + on-device OCR (digitization R2+R4)
Lower the bulk-digitization cliff with two more routes on top of the already-landed CSV import and "save and add another": - Photo-first drafts (capture now, catalogue later): burst-capture photos (camera or multi-gallery) into unnamed draft varieties, shown in a "to catalogue" tray, hidden from the main list until named. Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft, the triage sheet and the inventory banner. - On-device OCR label suggestion (Tesseract, offline, no Google): a "Suggest name from photo" button in the naming dialog behind a LabelTextExtractor interface (Tesseract on Android/iOS, no-op elsewhere). Reads the largest print via hOCR bounding boxes, drops boilerplate/low-confidence noise, preprocesses (grayscale, contrast, upscale) and sweeps rotations (0-315 deg) so tilted packets still read. Bundles tessdata_fast eng+spa; validated on-device against real packets. The photo is written to a temp file deleted immediately in a finally block (the plugin needs a path) - a bounded, documented exception to no-plaintext-at-rest. This commit also carries the co-developed schema evolution v5 to v8 that shares these files (organic flag, species viability years, crop calendar, lot provenance/abundance/preservation format, condition checks) plus their exports/migrations and i18n. Tests: CSV/draft/OCR unit + widget + migration green in isolation. Note: the full widget suite currently hangs (>10 min) - under investigation.
This commit is contained in:
parent
12a2ee2d64
commit
6809dc6143
89 changed files with 17141 additions and 228 deletions
|
|
@ -2,6 +2,7 @@ import 'package:commons_core/commons_core.dart' show QuantityKind;
|
|||
|
||||
import '../../db/database.dart';
|
||||
import '../../db/enums.dart';
|
||||
import '../../domain/crop_calendar.dart';
|
||||
import 'inventory_snapshot.dart';
|
||||
|
||||
/// Flattens an [InventorySnapshot] into a spreadsheet-friendly CSV — one row
|
||||
|
|
@ -20,6 +21,12 @@ class InventoryCsvCodec {
|
|||
'variety_notes',
|
||||
'vernacular_names',
|
||||
'external_links',
|
||||
'needs_reproduction',
|
||||
'sow_months',
|
||||
'transplant_months',
|
||||
'flowering_months',
|
||||
'fruiting_months',
|
||||
'seed_harvest_months',
|
||||
'lot_id',
|
||||
'lot_type',
|
||||
'harvest_year',
|
||||
|
|
@ -30,7 +37,13 @@ class InventoryCsvCodec {
|
|||
'presentation',
|
||||
'storage_location',
|
||||
'offer_status',
|
||||
'origin_name',
|
||||
'origin_place',
|
||||
'abundance',
|
||||
'preservation_format',
|
||||
'latest_germination_rate',
|
||||
'container_count',
|
||||
'desiccant_state',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
|
@ -54,6 +67,7 @@ class InventoryCsvCodec {
|
|||
.add(e.title == null ? e.url : '${e.title}|${e.url}');
|
||||
}
|
||||
final latestRateByLot = _latestGerminationRates(snapshot.germinationTests);
|
||||
final latestCheckByLot = _latestConditionChecks(snapshot.conditionChecks);
|
||||
|
||||
final lines = <String>[_header.map(_escape).join(',')];
|
||||
for (final v in snapshot.varieties) {
|
||||
|
|
@ -66,13 +80,19 @@ class InventoryCsvCodec {
|
|||
v.notes ?? '',
|
||||
(namesByVariety[v.id] ?? const []).join('; '),
|
||||
(linksByVariety[v.id] ?? const []).join('; '),
|
||||
v.needsReproduction ? 'true' : '',
|
||||
_monthsCell(v.sowMonths),
|
||||
_monthsCell(v.transplantMonths),
|
||||
_monthsCell(v.floweringMonths),
|
||||
_monthsCell(v.fruitingMonths),
|
||||
_monthsCell(v.seedHarvestMonths),
|
||||
];
|
||||
final lots = lotsByVariety[v.id] ?? const <Lot>[];
|
||||
if (lots.isEmpty) {
|
||||
lines.add(
|
||||
[
|
||||
...varietyCells,
|
||||
...List.filled(11, ''),
|
||||
...List.filled(17, ''),
|
||||
_isoDate(v.createdAt),
|
||||
_hlcMillisIso(v.updatedAt),
|
||||
].map(_escape).join(','),
|
||||
|
|
@ -80,6 +100,7 @@ class InventoryCsvCodec {
|
|||
continue;
|
||||
}
|
||||
for (final l in lots) {
|
||||
final check = latestCheckByLot[l.id];
|
||||
lines.add(
|
||||
[
|
||||
...varietyCells,
|
||||
|
|
@ -93,7 +114,13 @@ class InventoryCsvCodec {
|
|||
l.presentation?.name ?? '',
|
||||
l.storageLocation ?? '',
|
||||
l.offerStatus.name,
|
||||
l.originName ?? '',
|
||||
l.originPlace ?? '',
|
||||
l.abundance?.name ?? '',
|
||||
l.preservationFormat?.name ?? '',
|
||||
latestRateByLot[l.id]?.toStringAsFixed(2) ?? '',
|
||||
check?.containerCount?.toString() ?? '',
|
||||
check?.desiccantState?.name ?? '',
|
||||
_isoDate(l.createdAt),
|
||||
_hlcMillisIso(l.updatedAt),
|
||||
].map(_escape).join(','),
|
||||
|
|
@ -103,6 +130,25 @@ class InventoryCsvCodec {
|
|||
return '${lines.join('\r\n')}\r\n';
|
||||
}
|
||||
|
||||
/// Renders a crop-calendar month mask as a space-separated list of month
|
||||
/// numbers (e.g. `3 4 9`), or empty when unset — readable in a spreadsheet.
|
||||
String _monthsCell(int? mask) => maskToMonths(mask).join(' ');
|
||||
|
||||
/// lotId → most-recent condition check (by checkedOn), so the CSV can show a
|
||||
/// lot's latest jar count and drying-agent state as flat columns.
|
||||
Map<String, ConditionCheck> _latestConditionChecks(
|
||||
List<ConditionCheck> checks,
|
||||
) {
|
||||
final latest = <String, ConditionCheck>{};
|
||||
for (final c in checks) {
|
||||
final current = latest[c.lotId];
|
||||
if (current == null || (c.checkedOn ?? 0) > (current.checkedOn ?? 0)) {
|
||||
latest[c.lotId] = c;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
/// lotId → latest germination rate (0..1), from the most recent test that
|
||||
/// has a computable rate.
|
||||
Map<String, double> _latestGerminationRates(List<GerminationTest> tests) {
|
||||
|
|
@ -200,6 +246,12 @@ class InventoryCsvCodec {
|
|||
notes: cell(row, 'variety_notes'),
|
||||
vernacularNames: _parseVernacular(cell(row, 'vernacular_names')),
|
||||
links: _parseLinks(cell(row, 'external_links')),
|
||||
needsReproduction: _parseBool(cell(row, 'needs_reproduction')),
|
||||
sowMonths: _parseMonths(cell(row, 'sow_months')),
|
||||
transplantMonths: _parseMonths(cell(row, 'transplant_months')),
|
||||
floweringMonths: _parseMonths(cell(row, 'flowering_months')),
|
||||
fruitingMonths: _parseMonths(cell(row, 'fruiting_months')),
|
||||
seedHarvestMonths: _parseMonths(cell(row, 'seed_harvest_months')),
|
||||
);
|
||||
builders.add(builder);
|
||||
if (groupId != null) byGroup[groupId] = builder;
|
||||
|
|
@ -225,6 +277,10 @@ class InventoryCsvCodec {
|
|||
final presentation = cell(row, 'presentation');
|
||||
final storage = cell(row, 'storage_location');
|
||||
final offer = cell(row, 'offer_status');
|
||||
final originName = cell(row, 'origin_name');
|
||||
final originPlace = cell(row, 'origin_place');
|
||||
final abundance = cell(row, 'abundance');
|
||||
final preservationFormat = cell(row, 'preservation_format');
|
||||
|
||||
final hasLot = [
|
||||
typeCell,
|
||||
|
|
@ -236,6 +292,10 @@ class InventoryCsvCodec {
|
|||
presentation,
|
||||
storage,
|
||||
offer,
|
||||
originName,
|
||||
originPlace,
|
||||
abundance,
|
||||
preservationFormat,
|
||||
].any((c) => c != null);
|
||||
if (!hasLot) return null;
|
||||
|
||||
|
|
@ -249,9 +309,35 @@ class InventoryCsvCodec {
|
|||
presentation: _enumOrNull(Presentation.values, presentation),
|
||||
storageLocation: storage,
|
||||
offerStatus: _enumOr(OfferStatus.values, offer, OfferStatus.private),
|
||||
originName: originName,
|
||||
originPlace: originPlace,
|
||||
abundance: _enumOrNull(Abundance.values, abundance),
|
||||
preservationFormat: _enumOrNull(
|
||||
PreservationFormat.values,
|
||||
preservationFormat,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses a boolean cell: `true`/`yes`/`1` (case-insensitive) → true,
|
||||
/// anything else (including null) → false.
|
||||
bool _parseBool(String? cell) {
|
||||
final v = cell?.trim().toLowerCase();
|
||||
return v == 'true' || v == 'yes' || v == '1';
|
||||
}
|
||||
|
||||
/// Parses a crop-calendar months cell (month numbers separated by spaces,
|
||||
/// commas or semicolons, e.g. `3 4 9`) into a 12-bit mask, or null when empty.
|
||||
int? _parseMonths(String? cell) {
|
||||
if (cell == null) return null;
|
||||
final months = <int>[];
|
||||
for (final part in cell.split(RegExp(r'[\s,;]+'))) {
|
||||
final n = int.tryParse(part.trim());
|
||||
if (n != null) months.add(n);
|
||||
}
|
||||
return monthsToMask(months);
|
||||
}
|
||||
|
||||
/// `name (lang); name2` → vernacular names; the trailing `(xx)` is the ISO
|
||||
/// language, mirroring the export join in [encode].
|
||||
List<CsvImportName> _parseVernacular(String? cell) {
|
||||
|
|
@ -393,6 +479,12 @@ class CsvImportVariety {
|
|||
this.vernacularNames = const [],
|
||||
this.links = const [],
|
||||
this.lots = const [],
|
||||
this.needsReproduction = false,
|
||||
this.sowMonths,
|
||||
this.transplantMonths,
|
||||
this.floweringMonths,
|
||||
this.fruitingMonths,
|
||||
this.seedHarvestMonths,
|
||||
});
|
||||
|
||||
final String label;
|
||||
|
|
@ -403,6 +495,12 @@ class CsvImportVariety {
|
|||
final List<CsvImportName> vernacularNames;
|
||||
final List<CsvImportLink> links;
|
||||
final List<CsvImportLot> lots;
|
||||
final bool needsReproduction;
|
||||
final int? sowMonths;
|
||||
final int? transplantMonths;
|
||||
final int? floweringMonths;
|
||||
final int? fruitingMonths;
|
||||
final int? seedHarvestMonths;
|
||||
}
|
||||
|
||||
/// One lot parsed from a CSV row.
|
||||
|
|
@ -417,6 +515,10 @@ class CsvImportLot {
|
|||
this.presentation,
|
||||
this.storageLocation,
|
||||
this.offerStatus = OfferStatus.private,
|
||||
this.originName,
|
||||
this.originPlace,
|
||||
this.abundance,
|
||||
this.preservationFormat,
|
||||
});
|
||||
|
||||
final LotType type;
|
||||
|
|
@ -428,6 +530,10 @@ class CsvImportLot {
|
|||
final Presentation? presentation;
|
||||
final String? storageLocation;
|
||||
final OfferStatus offerStatus;
|
||||
final String? originName;
|
||||
final String? originPlace;
|
||||
final Abundance? abundance;
|
||||
final PreservationFormat? preservationFormat;
|
||||
}
|
||||
|
||||
/// One vernacular name parsed from CSV.
|
||||
|
|
@ -456,6 +562,12 @@ class _CsvVarietyBuilder {
|
|||
this.notes,
|
||||
this.vernacularNames = const [],
|
||||
this.links = const [],
|
||||
this.needsReproduction = false,
|
||||
this.sowMonths,
|
||||
this.transplantMonths,
|
||||
this.floweringMonths,
|
||||
this.fruitingMonths,
|
||||
this.seedHarvestMonths,
|
||||
});
|
||||
|
||||
final String label;
|
||||
|
|
@ -465,6 +577,12 @@ class _CsvVarietyBuilder {
|
|||
final String? notes;
|
||||
final List<CsvImportName> vernacularNames;
|
||||
final List<CsvImportLink> links;
|
||||
final bool needsReproduction;
|
||||
final int? sowMonths;
|
||||
final int? transplantMonths;
|
||||
final int? floweringMonths;
|
||||
final int? fruitingMonths;
|
||||
final int? seedHarvestMonths;
|
||||
final List<CsvImportLot> lots = [];
|
||||
|
||||
CsvImportVariety build() => CsvImportVariety(
|
||||
|
|
@ -475,6 +593,12 @@ class _CsvVarietyBuilder {
|
|||
notes: notes,
|
||||
vernacularNames: vernacularNames,
|
||||
links: links,
|
||||
needsReproduction: needsReproduction,
|
||||
sowMonths: sowMonths,
|
||||
transplantMonths: transplantMonths,
|
||||
floweringMonths: floweringMonths,
|
||||
fruitingMonths: fruitingMonths,
|
||||
seedHarvestMonths: seedHarvestMonths,
|
||||
lots: List.unmodifiable(lots),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ class InventoryJsonCodec {
|
|||
'cultivarName': v.cultivarName,
|
||||
'category': v.category,
|
||||
'notes': v.notes,
|
||||
'isDraft': v.isDraft,
|
||||
'isOrganic': v.isOrganic,
|
||||
'needsReproduction': v.needsReproduction,
|
||||
'sowMonths': v.sowMonths,
|
||||
'transplantMonths': v.transplantMonths,
|
||||
'floweringMonths': v.floweringMonths,
|
||||
'fruitingMonths': v.fruitingMonths,
|
||||
'seedHarvestMonths': v.seedHarvestMonths,
|
||||
},
|
||||
],
|
||||
'lots': [
|
||||
|
|
@ -65,6 +73,10 @@ class InventoryJsonCodec {
|
|||
'storageLocation': l.storageLocation,
|
||||
'offerStatus': l.offerStatus.name,
|
||||
'seedbankId': l.seedbankId,
|
||||
'originName': l.originName,
|
||||
'originPlace': l.originPlace,
|
||||
'abundance': l.abundance?.name,
|
||||
'preservationFormat': l.preservationFormat?.name,
|
||||
},
|
||||
],
|
||||
'vernacularNames': [
|
||||
|
|
@ -116,6 +128,23 @@ class InventoryJsonCodec {
|
|||
'notes': g.notes,
|
||||
},
|
||||
],
|
||||
'conditionChecks': [
|
||||
for (final c in snapshot.conditionChecks)
|
||||
{
|
||||
..._syncMeta(
|
||||
id: c.id,
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
lastAuthor: c.lastAuthor,
|
||||
schemaRowVersion: c.schemaRowVersion,
|
||||
),
|
||||
'lotId': c.lotId,
|
||||
'checkedOn': c.checkedOn,
|
||||
'containerCount': c.containerCount,
|
||||
'desiccantState': c.desiccantState?.name,
|
||||
'notes': c.notes,
|
||||
},
|
||||
],
|
||||
'movements': [
|
||||
for (final m in snapshot.movements)
|
||||
{
|
||||
|
|
@ -214,6 +243,14 @@ class InventoryJsonCodec {
|
|||
cultivarName: m['cultivarName'] as String?,
|
||||
category: m['category'] as String?,
|
||||
notes: m['notes'] as String?,
|
||||
isDraft: m['isDraft'] == true,
|
||||
isOrganic: m['isOrganic'] == true,
|
||||
needsReproduction: m['needsReproduction'] == true,
|
||||
sowMonths: m['sowMonths'] as int?,
|
||||
transplantMonths: m['transplantMonths'] as int?,
|
||||
floweringMonths: m['floweringMonths'] as int?,
|
||||
fruitingMonths: m['fruitingMonths'] as int?,
|
||||
seedHarvestMonths: m['seedHarvestMonths'] as int?,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -244,6 +281,13 @@ class InventoryJsonCodec {
|
|||
OfferStatus.private,
|
||||
),
|
||||
seedbankId: m['seedbankId'] as String?,
|
||||
originName: m['originName'] as String?,
|
||||
originPlace: m['originPlace'] as String?,
|
||||
abundance: _enumOrNull(Abundance.values, m['abundance']),
|
||||
preservationFormat: _enumOrNull(
|
||||
PreservationFormat.values,
|
||||
m['preservationFormat'],
|
||||
),
|
||||
);
|
||||
}),
|
||||
vernacularNames: _rows(root, 'vernacularNames', (m) {
|
||||
|
|
@ -291,6 +335,24 @@ class InventoryJsonCodec {
|
|||
notes: m['notes'] as String?,
|
||||
);
|
||||
}),
|
||||
conditionChecks: _rows(root, 'conditionChecks', (m) {
|
||||
return ConditionCheck(
|
||||
id: _string(m, 'id'),
|
||||
createdAt: _int(m, 'createdAt'),
|
||||
updatedAt: _string(m, 'updatedAt'),
|
||||
lastAuthor: _string(m, 'lastAuthor'),
|
||||
isDeleted: false,
|
||||
schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
|
||||
lotId: _string(m, 'lotId'),
|
||||
checkedOn: m['checkedOn'] as int?,
|
||||
containerCount: m['containerCount'] as int?,
|
||||
desiccantState: _enumOrNull(
|
||||
DesiccantState.values,
|
||||
m['desiccantState'],
|
||||
),
|
||||
notes: m['notes'] as String?,
|
||||
);
|
||||
}),
|
||||
movements: _rows(root, 'movements', (m) {
|
||||
final type = _enumOrNull(MovementType.values, m['type']);
|
||||
if (type == null) return null; // no safe default → drop row
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class InventorySnapshot {
|
|||
this.vernacularNames = const [],
|
||||
this.externalLinks = const [],
|
||||
this.germinationTests = const [],
|
||||
this.conditionChecks = const [],
|
||||
this.movements = const [],
|
||||
this.parties = const [],
|
||||
this.attachments = const [],
|
||||
|
|
@ -37,6 +38,7 @@ class InventorySnapshot {
|
|||
final List<VarietyVernacularName> vernacularNames;
|
||||
final List<ExternalLink> externalLinks;
|
||||
final List<GerminationTest> germinationTests;
|
||||
final List<ConditionCheck> conditionChecks;
|
||||
final List<Movement> movements;
|
||||
final List<Party> parties;
|
||||
final List<Attachment> attachments;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ List<SpeciesSeed> parseSpeciesCatalog(String jsonString) {
|
|||
scientificName: e['scientific_name'] as String,
|
||||
family: e['family'] as String?,
|
||||
commonNames: common,
|
||||
viabilityYears: (e['viability_years'] as num?)?.toInt(),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class SpeciesSeed {
|
|||
required this.scientificName,
|
||||
this.family,
|
||||
this.commonNames = const {},
|
||||
this.viabilityYears,
|
||||
});
|
||||
|
||||
final String scientificName;
|
||||
|
|
@ -16,6 +17,9 @@ class SpeciesSeed {
|
|||
|
||||
/// language code → list of common names.
|
||||
final Map<String, List<String>> commonNames;
|
||||
|
||||
/// Typical seed longevity in years (bundled reference data); null if unknown.
|
||||
final int? viabilityYears;
|
||||
}
|
||||
|
||||
/// A catalog match surfaced to the UI (with a best common name for the locale).
|
||||
|
|
@ -47,7 +51,9 @@ class SpeciesRepository {
|
|||
final String nodeId;
|
||||
|
||||
/// Idempotently inserts bundled species (keyed by scientific name). Safe to
|
||||
/// call on every startup; existing entries are left untouched.
|
||||
/// call on every startup. Existing rows keep their identity, but a bundled
|
||||
/// row still missing [SpeciesSeed.viabilityYears] is backfilled — so the
|
||||
/// reference data reaches installs seeded before the field was bundled.
|
||||
Future<void> seedBundled(List<SpeciesSeed> seeds) async {
|
||||
final stamp = Hlc.zero(nodeId).pack();
|
||||
await _db.transaction(() async {
|
||||
|
|
@ -56,7 +62,19 @@ class SpeciesRepository {
|
|||
await (_db.select(_db.species)
|
||||
..where((s) => s.scientificName.equals(seed.scientificName)))
|
||||
.getSingleOrNull();
|
||||
if (existing != null) continue;
|
||||
if (existing != null) {
|
||||
// Backfill bundled reference data added after the row was seeded.
|
||||
if (existing.viabilityYears == null && seed.viabilityYears != null) {
|
||||
await (_db.update(_db.species)
|
||||
..where((s) => s.id.equals(existing.id)))
|
||||
.write(
|
||||
SpeciesCompanion(
|
||||
viabilityYears: Value(seed.viabilityYears),
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
final speciesId = idGen.newId();
|
||||
await _db
|
||||
|
|
@ -70,6 +88,7 @@ class SpeciesRepository {
|
|||
scientificName: seed.scientificName,
|
||||
family: Value(seed.family),
|
||||
isBundled: const Value(true),
|
||||
viabilityYears: Value(seed.viabilityYears),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart';
|
|||
|
||||
import '../db/database.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../domain/seed_viability.dart';
|
||||
import 'export_import/import_reconciler.dart';
|
||||
import 'export_import/inventory_csv_codec.dart';
|
||||
import 'export_import/inventory_snapshot.dart';
|
||||
|
|
@ -18,6 +19,10 @@ class VarietyListItem extends Equatable {
|
|||
this.scientificName,
|
||||
this.photo,
|
||||
this.lotTypes = const {},
|
||||
this.isDraft = false,
|
||||
this.isOrganic = false,
|
||||
this.needsReproduction = false,
|
||||
this.viability = SeedViability.unknown,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
|
@ -34,6 +39,23 @@ class VarietyListItem extends Equatable {
|
|||
/// used to filter the list by form. Empty when it has no lots yet.
|
||||
final Set<LotType> lotTypes;
|
||||
|
||||
/// True for a photo-first capture still awaiting a name (in the "to
|
||||
/// catalogue" tray, hidden from the main inventory list).
|
||||
final bool isDraft;
|
||||
|
||||
/// Grower-declared organic ("eco") provenance — badged and filterable.
|
||||
final bool isOrganic;
|
||||
|
||||
/// Grower-flagged "regrow this season" stewardship intent — badged and
|
||||
/// filterable, alongside the automatic viability warning.
|
||||
final bool needsReproduction;
|
||||
|
||||
/// The most-urgent viability status across this variety's seed lots
|
||||
/// (expired ranks above expiringSoon), so the list can flag what to sow or
|
||||
/// reproduce before it lapses. [SeedViability.unknown] when nothing is aging
|
||||
/// or there is no reference figure.
|
||||
final SeedViability viability;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
|
|
@ -42,6 +64,10 @@ class VarietyListItem extends Equatable {
|
|||
scientificName,
|
||||
photo,
|
||||
lotTypes,
|
||||
isDraft,
|
||||
isOrganic,
|
||||
needsReproduction,
|
||||
viability,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -73,8 +99,35 @@ class GerminationEntry extends Equatable {
|
|||
List<Object?> get props => [id, testedOn, sampleSize, germinatedCount, notes];
|
||||
}
|
||||
|
||||
/// One held batch of a variety, for the detail view. [germinationTests] are
|
||||
/// ordered most-recent first, so `germinationTests.first` is the latest.
|
||||
/// One storage-condition check on a lot: how many containers held it and the
|
||||
/// state of the drying agent at a point in time.
|
||||
class ConditionEntry extends Equatable {
|
||||
const ConditionEntry({
|
||||
required this.id,
|
||||
this.checkedOn,
|
||||
this.containerCount,
|
||||
this.desiccantState,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final int? checkedOn; // ms since epoch
|
||||
final int? containerCount;
|
||||
final DesiccantState? desiccantState;
|
||||
final String? notes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
checkedOn,
|
||||
containerCount,
|
||||
desiccantState,
|
||||
notes,
|
||||
];
|
||||
}
|
||||
|
||||
/// One held batch of a variety, for the detail view. [germinationTests] and
|
||||
/// [conditionChecks] are ordered most-recent first, so `.first` is the latest.
|
||||
class VarietyLot extends Equatable {
|
||||
const VarietyLot({
|
||||
required this.id,
|
||||
|
|
@ -84,7 +137,12 @@ class VarietyLot extends Equatable {
|
|||
this.quantity,
|
||||
this.presentation,
|
||||
this.storageLocation,
|
||||
this.originName,
|
||||
this.originPlace,
|
||||
this.abundance,
|
||||
this.preservationFormat,
|
||||
this.germinationTests = const [],
|
||||
this.conditionChecks = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
|
@ -98,8 +156,23 @@ class VarietyLot extends Equatable {
|
|||
/// How living material is packaged (pot, tray, bare-root…); null for seeds.
|
||||
final Presentation? presentation;
|
||||
final String? storageLocation;
|
||||
|
||||
/// Provenance of this batch: who grew/gave it ([originName]) and where it
|
||||
/// comes from ([originPlace], with region/province). Optional free text.
|
||||
final String? originName;
|
||||
final String? originPlace;
|
||||
|
||||
/// Optional coarse "how much I have" fusing amount with shareability.
|
||||
final Abundance? abundance;
|
||||
|
||||
/// How the (seed) lot is physically conserved; distinct from storage location.
|
||||
final PreservationFormat? preservationFormat;
|
||||
|
||||
final List<GerminationEntry> germinationTests;
|
||||
|
||||
/// Storage-condition checks, most-recent first (`conditionChecks.first` = last).
|
||||
final List<ConditionEntry> conditionChecks;
|
||||
|
||||
/// The most recent germination rate (0..1), or null if there are no tests.
|
||||
double? get latestGerminationRate =>
|
||||
germinationTests.isEmpty ? null : germinationTests.first.rate;
|
||||
|
|
@ -113,7 +186,12 @@ class VarietyLot extends Equatable {
|
|||
quantity,
|
||||
presentation,
|
||||
storageLocation,
|
||||
originName,
|
||||
originPlace,
|
||||
abundance,
|
||||
preservationFormat,
|
||||
germinationTests,
|
||||
conditionChecks,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +249,14 @@ class VarietyDetail extends Equatable {
|
|||
this.notes,
|
||||
this.speciesId,
|
||||
this.scientificName,
|
||||
this.viabilityYears,
|
||||
this.isOrganic = false,
|
||||
this.needsReproduction = false,
|
||||
this.sowMonths,
|
||||
this.transplantMonths,
|
||||
this.floweringMonths,
|
||||
this.fruitingMonths,
|
||||
this.seedHarvestMonths,
|
||||
this.lots = const [],
|
||||
this.vernacularNames = const [],
|
||||
this.photos = const [],
|
||||
|
|
@ -183,6 +269,35 @@ class VarietyDetail extends Equatable {
|
|||
final String? notes;
|
||||
final String? speciesId;
|
||||
final String? scientificName;
|
||||
|
||||
/// Grower-declared organic ("eco") provenance.
|
||||
final bool isOrganic;
|
||||
|
||||
/// Grower-flagged "regrow this season" stewardship intent.
|
||||
final bool needsReproduction;
|
||||
|
||||
/// Advisory crop-calendar months, packed as 12-bit masks (a phase can span
|
||||
/// several months); null where unrecorded. See [Varieties.sowMonths] et al.
|
||||
/// and `domain/crop_calendar.dart` for packing.
|
||||
final int? sowMonths;
|
||||
final int? transplantMonths;
|
||||
final int? floweringMonths;
|
||||
final int? fruitingMonths;
|
||||
final int? seedHarvestMonths;
|
||||
|
||||
/// True when any crop-calendar phase is recorded — so the UI shows the
|
||||
/// calendar section only when there is something in it.
|
||||
bool get hasCropCalendar =>
|
||||
sowMonths != null ||
|
||||
transplantMonths != null ||
|
||||
floweringMonths != null ||
|
||||
fruitingMonths != null ||
|
||||
seedHarvestMonths != null;
|
||||
|
||||
/// Typical seed longevity (years) of the linked species, from the bundled
|
||||
/// catalog; null when no species is linked or the figure is unknown. Drives
|
||||
/// the per-lot viability warning together with each lot's harvest year.
|
||||
final int? viabilityYears;
|
||||
final List<VarietyLot> lots;
|
||||
final List<VernacularName> vernacularNames;
|
||||
final List<VarietyPhoto> photos;
|
||||
|
|
@ -196,6 +311,14 @@ class VarietyDetail extends Equatable {
|
|||
notes,
|
||||
speciesId,
|
||||
scientificName,
|
||||
viabilityYears,
|
||||
isOrganic,
|
||||
needsReproduction,
|
||||
sowMonths,
|
||||
transplantMonths,
|
||||
floweringMonths,
|
||||
fruitingMonths,
|
||||
seedHarvestMonths,
|
||||
lots,
|
||||
vernacularNames,
|
||||
photos,
|
||||
|
|
@ -245,18 +368,51 @@ class VarietyRepository {
|
|||
Future<List<VarietyListItem>> _loadInventory() async {
|
||||
final rows =
|
||||
await (_db.select(_db.varieties)
|
||||
..where((v) => v.isDeleted.equals(false))
|
||||
// Drafts live in the "to catalogue" tray, not the main list.
|
||||
..where((v) => v.isDeleted.equals(false) & v.isDraft.equals(false))
|
||||
..orderBy([
|
||||
(v) => OrderingTerm(expression: v.category),
|
||||
(v) => OrderingTerm(expression: v.label),
|
||||
]))
|
||||
.get();
|
||||
return _toListItems(rows);
|
||||
}
|
||||
|
||||
/// Reactively watches the photo-first drafts awaiting a name, newest first —
|
||||
/// the "to catalogue" tray. Re-emits when a draft is added, named or its
|
||||
/// photo changes.
|
||||
Stream<List<VarietyListItem>> watchDrafts() {
|
||||
final triggers = StreamGroup.merge<void>([
|
||||
(_db.select(_db.varieties)..where((v) => v.isDraft.equals(true)))
|
||||
.watch()
|
||||
.map((_) {}),
|
||||
_db.select(_db.attachments).watch().map((_) {}),
|
||||
]);
|
||||
return triggers.asyncMap((_) => _loadDrafts());
|
||||
}
|
||||
|
||||
Future<List<VarietyListItem>> _loadDrafts() async {
|
||||
final rows =
|
||||
await (_db.select(_db.varieties)
|
||||
..where(
|
||||
(v) => v.isDeleted.equals(false) & v.isDraft.equals(true),
|
||||
)
|
||||
// Packed HLC sorts monotonically, so newest capture is first even
|
||||
// when two are stamped within the same wall-clock millisecond.
|
||||
..orderBy([(v) => OrderingTerm.desc(v.updatedAt)]))
|
||||
.get();
|
||||
return _toListItems(rows);
|
||||
}
|
||||
|
||||
Future<List<VarietyListItem>> _toListItems(List<Variety> rows) async {
|
||||
final varietyIds = rows.map((v) => v.id).toList();
|
||||
final speciesIds = rows.map((v) => v.speciesId).whereType<String>().toSet();
|
||||
final photos = await _firstPhotosFor(varietyIds);
|
||||
final sciNames = await _scientificNamesFor(
|
||||
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
||||
);
|
||||
final sciNames = await _scientificNamesFor(speciesIds);
|
||||
final speciesViability = await _speciesViabilityFor(speciesIds);
|
||||
final lotTypes = await _lotTypesFor(varietyIds);
|
||||
final seedYears = await _seedHarvestYearsFor(varietyIds);
|
||||
final currentYear = DateTime.fromMillisecondsSinceEpoch(_now()).year;
|
||||
return rows
|
||||
.map(
|
||||
(v) => VarietyListItem(
|
||||
|
|
@ -266,11 +422,82 @@ class VarietyRepository {
|
|||
scientificName: v.speciesId == null ? null : sciNames[v.speciesId],
|
||||
photo: photos[v.id],
|
||||
lotTypes: lotTypes[v.id] ?? const {},
|
||||
isDraft: v.isDraft,
|
||||
isOrganic: v.isOrganic,
|
||||
needsReproduction: v.needsReproduction,
|
||||
viability: _worstViability(
|
||||
harvestYears: seedYears[v.id] ?? const [],
|
||||
viabilityYears: v.speciesId == null
|
||||
? null
|
||||
: speciesViability[v.speciesId],
|
||||
currentYear: currentYear,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Maps each of [speciesIds] to its bundled viability figure (years); species
|
||||
/// without a figure are simply absent from the map.
|
||||
Future<Map<String, int>> _speciesViabilityFor(Set<String> speciesIds) async {
|
||||
if (speciesIds.isEmpty) return const {};
|
||||
final rows = await (_db.select(
|
||||
_db.species,
|
||||
)..where((s) => s.id.isIn(speciesIds))).get();
|
||||
return {
|
||||
for (final s in rows)
|
||||
if (s.viabilityYears != null) s.id: s.viabilityYears!,
|
||||
};
|
||||
}
|
||||
|
||||
/// Harvest years of each variety's non-deleted seed lots (living lots don't
|
||||
/// age like dry seed). Lots without a year are omitted — they can't be judged.
|
||||
Future<Map<String, List<int>>> _seedHarvestYearsFor(
|
||||
List<String> varietyIds,
|
||||
) async {
|
||||
if (varietyIds.isEmpty) return const {};
|
||||
final rows =
|
||||
await (_db.select(_db.lots)..where(
|
||||
(l) =>
|
||||
l.varietyId.isIn(varietyIds) &
|
||||
l.isDeleted.equals(false) &
|
||||
l.type.equalsValue(LotType.seed),
|
||||
))
|
||||
.get();
|
||||
final byVariety = <String, List<int>>{};
|
||||
for (final row in rows) {
|
||||
final year = row.harvestYear;
|
||||
if (year != null) (byVariety[row.varietyId] ??= <int>[]).add(year);
|
||||
}
|
||||
return byVariety;
|
||||
}
|
||||
|
||||
/// The most-urgent viability across a variety's seed lots (expired outranks
|
||||
/// expiring-soon), so the list surfaces the worst case at a glance.
|
||||
SeedViability _worstViability({
|
||||
required List<int> harvestYears,
|
||||
required int? viabilityYears,
|
||||
required int currentYear,
|
||||
}) {
|
||||
var worst = SeedViability.unknown;
|
||||
for (final year in harvestYears) {
|
||||
final status = seedViability(
|
||||
harvestYear: year,
|
||||
viabilityYears: viabilityYears,
|
||||
currentYear: currentYear,
|
||||
);
|
||||
if (_viabilityRank(status) > _viabilityRank(worst)) worst = status;
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
int _viabilityRank(SeedViability status) => switch (status) {
|
||||
SeedViability.unknown => 0,
|
||||
SeedViability.fresh => 1,
|
||||
SeedViability.expiringSoon => 2,
|
||||
SeedViability.expired => 3,
|
||||
};
|
||||
|
||||
/// Maps each variety to the distinct lot forms it currently holds (one
|
||||
/// query). Varieties without lots are simply absent from the map.
|
||||
Future<Map<String, Set<LotType>>> _lotTypesFor(
|
||||
|
|
@ -394,6 +621,59 @@ class VarietyRepository {
|
|||
return varietyId;
|
||||
}
|
||||
|
||||
/// Captures a photo-first draft: a Variety with no name yet ([label] empty),
|
||||
/// flagged [isDraft], plus its photo. It lands in the "to catalogue" tray
|
||||
/// ([watchDrafts]) until the user names it with [nameDraft]. Returns the id.
|
||||
Future<String> addDraftVariety(Uint8List photoBytes) async {
|
||||
final varietyId = idGen.newId();
|
||||
await _db.transaction(() async {
|
||||
final (created, updated) = _stamp();
|
||||
await _db
|
||||
.into(_db.varieties)
|
||||
.insert(
|
||||
VarietiesCompanion.insert(
|
||||
id: varietyId,
|
||||
label: '',
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
isDraft: const Value(true),
|
||||
),
|
||||
);
|
||||
final (createdA, updatedA) = _stamp();
|
||||
await _db
|
||||
.into(_db.attachments)
|
||||
.insert(
|
||||
AttachmentsCompanion.insert(
|
||||
id: idGen.newId(),
|
||||
createdAt: createdA,
|
||||
updatedAt: updatedA,
|
||||
lastAuthor: nodeId,
|
||||
parentType: ParentType.variety,
|
||||
parentId: varietyId,
|
||||
kind: AttachmentKind.photo,
|
||||
bytes: Value(photoBytes),
|
||||
mimeType: const Value('image/jpeg'),
|
||||
),
|
||||
);
|
||||
});
|
||||
return varietyId;
|
||||
}
|
||||
|
||||
/// Names a draft and promotes it out of the "to catalogue" tray: sets its
|
||||
/// [label] and clears [isDraft] in one LWW write.
|
||||
Future<void> nameDraft(String id, String label) async {
|
||||
final (_, updated) = _stamp();
|
||||
await (_db.update(_db.varieties)..where((v) => v.id.equals(id))).write(
|
||||
VarietiesCompanion(
|
||||
label: Value(label),
|
||||
isDraft: const Value(false),
|
||||
updatedAt: Value(updated),
|
||||
lastAuthor: Value(nodeId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Reactively watches one variety with its lots, vernacular names and first
|
||||
/// photo. Emits `null` if the variety does not exist or is soft-deleted.
|
||||
///
|
||||
|
|
@ -419,6 +699,8 @@ class VarietyRepository {
|
|||
)..where((e) => e.parentId.equals(id))).watch().map((_) {}),
|
||||
// Coarse: any germination-test change re-emits (catalog of tests is tiny).
|
||||
_db.select(_db.germinationTests).watch().map((_) {}),
|
||||
// Same for condition checks (jars + drying-agent state).
|
||||
_db.select(_db.conditionChecks).watch().map((_) {}),
|
||||
]);
|
||||
return triggers.asyncMap((_) => _loadVariety(id));
|
||||
}
|
||||
|
|
@ -431,11 +713,13 @@ class VarietyRepository {
|
|||
if (v == null) return null;
|
||||
|
||||
String? scientificName;
|
||||
int? viabilityYears;
|
||||
if (v.speciesId != null) {
|
||||
final species = await (_db.select(
|
||||
_db.species,
|
||||
)..where((s) => s.id.equals(v.speciesId!))).getSingleOrNull();
|
||||
scientificName = species?.scientificName;
|
||||
viabilityYears = species?.viabilityYears;
|
||||
}
|
||||
|
||||
final lots =
|
||||
|
|
@ -446,6 +730,7 @@ class VarietyRepository {
|
|||
|
||||
final lotIds = lots.map((l) => l.id).toList();
|
||||
final testsByLot = <String, List<GerminationEntry>>{};
|
||||
final checksByLot = <String, List<ConditionEntry>>{};
|
||||
if (lotIds.isNotEmpty) {
|
||||
final tests =
|
||||
await (_db.select(_db.germinationTests)
|
||||
|
|
@ -465,6 +750,24 @@ class VarietyRepository {
|
|||
),
|
||||
);
|
||||
}
|
||||
final checks =
|
||||
await (_db.select(_db.conditionChecks)
|
||||
..where((c) => c.lotId.isIn(lotIds) & c.isDeleted.equals(false))
|
||||
..orderBy([(c) => OrderingTerm.desc(c.checkedOn)]))
|
||||
.get();
|
||||
for (final c in checks) {
|
||||
checksByLot
|
||||
.putIfAbsent(c.lotId, () => [])
|
||||
.add(
|
||||
ConditionEntry(
|
||||
id: c.id,
|
||||
checkedOn: c.checkedOn,
|
||||
containerCount: c.containerCount,
|
||||
desiccantState: c.desiccantState,
|
||||
notes: c.notes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final names = await (_db.select(
|
||||
|
|
@ -503,7 +806,23 @@ class VarietyRepository {
|
|||
notes: v.notes,
|
||||
speciesId: v.speciesId,
|
||||
scientificName: scientificName,
|
||||
lots: lots.map((l) => _toLot(l, testsByLot[l.id] ?? const [])).toList(),
|
||||
viabilityYears: viabilityYears,
|
||||
isOrganic: v.isOrganic,
|
||||
needsReproduction: v.needsReproduction,
|
||||
sowMonths: v.sowMonths,
|
||||
transplantMonths: v.transplantMonths,
|
||||
floweringMonths: v.floweringMonths,
|
||||
fruitingMonths: v.fruitingMonths,
|
||||
seedHarvestMonths: v.seedHarvestMonths,
|
||||
lots: lots
|
||||
.map(
|
||||
(l) => _toLot(
|
||||
l,
|
||||
testsByLot[l.id] ?? const [],
|
||||
checksByLot[l.id] ?? const [],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
vernacularNames: names
|
||||
.map(
|
||||
(n) => VernacularName(
|
||||
|
|
@ -552,12 +871,21 @@ class VarietyRepository {
|
|||
}
|
||||
|
||||
/// Updates a variety's scalar fields (LWW). Passing null clears [category]
|
||||
/// and [notes]; a null [label] leaves the label unchanged.
|
||||
/// and [notes]; a null [label] leaves the label unchanged. Boolean flags and
|
||||
/// the crop-calendar months are only touched when a wrapping [Value] is
|
||||
/// passed, so callers can update one field without disturbing the rest.
|
||||
Future<void> updateVariety({
|
||||
required String id,
|
||||
String? label,
|
||||
String? category,
|
||||
String? notes,
|
||||
bool? isOrganic,
|
||||
bool? needsReproduction,
|
||||
Value<int?> sowMonths = const Value.absent(),
|
||||
Value<int?> transplantMonths = const Value.absent(),
|
||||
Value<int?> floweringMonths = const Value.absent(),
|
||||
Value<int?> fruitingMonths = const Value.absent(),
|
||||
Value<int?> seedHarvestMonths = const Value.absent(),
|
||||
}) async {
|
||||
final (_, updated) = _stamp();
|
||||
await (_db.update(_db.varieties)..where((v) => v.id.equals(id))).write(
|
||||
|
|
@ -565,6 +893,15 @@ class VarietyRepository {
|
|||
label: label == null ? const Value.absent() : Value(label),
|
||||
category: Value(category),
|
||||
notes: Value(notes),
|
||||
isOrganic: isOrganic == null ? const Value.absent() : Value(isOrganic),
|
||||
needsReproduction: needsReproduction == null
|
||||
? const Value.absent()
|
||||
: Value(needsReproduction),
|
||||
sowMonths: sowMonths,
|
||||
transplantMonths: transplantMonths,
|
||||
floweringMonths: floweringMonths,
|
||||
fruitingMonths: fruitingMonths,
|
||||
seedHarvestMonths: seedHarvestMonths,
|
||||
updatedAt: Value(updated),
|
||||
lastAuthor: Value(nodeId),
|
||||
),
|
||||
|
|
@ -580,6 +917,10 @@ class VarietyRepository {
|
|||
Quantity? quantity,
|
||||
Presentation? presentation,
|
||||
String? storageLocation,
|
||||
String? originName,
|
||||
String? originPlace,
|
||||
Abundance? abundance,
|
||||
PreservationFormat? preservationFormat,
|
||||
}) async {
|
||||
final (created, updated) = _stamp();
|
||||
final id = idGen.newId();
|
||||
|
|
@ -600,13 +941,17 @@ class VarietyRepository {
|
|||
quantityLabel: Value(quantity?.label),
|
||||
presentation: Value(presentation),
|
||||
storageLocation: Value(storageLocation),
|
||||
originName: Value(originName),
|
||||
originPlace: Value(originPlace),
|
||||
abundance: Value(abundance),
|
||||
preservationFormat: Value(preservationFormat),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Updates an existing lot's fields (LWW). The [quantity], [harvestYear] and
|
||||
/// [harvestMonth] are set as given (null clears them).
|
||||
/// Updates an existing lot's fields (LWW). Every field is set as given (null
|
||||
/// clears it), matching the "save the whole lot form" call from the UI.
|
||||
Future<void> updateLot({
|
||||
required String lotId,
|
||||
required LotType type,
|
||||
|
|
@ -615,6 +960,10 @@ class VarietyRepository {
|
|||
Quantity? quantity,
|
||||
Presentation? presentation,
|
||||
String? storageLocation,
|
||||
String? originName,
|
||||
String? originPlace,
|
||||
Abundance? abundance,
|
||||
PreservationFormat? preservationFormat,
|
||||
}) async {
|
||||
final (_, updated) = _stamp();
|
||||
await (_db.update(_db.lots)..where((l) => l.id.equals(lotId))).write(
|
||||
|
|
@ -627,6 +976,10 @@ class VarietyRepository {
|
|||
quantityLabel: Value(quantity?.label),
|
||||
presentation: Value(presentation),
|
||||
storageLocation: Value(storageLocation),
|
||||
originName: Value(originName),
|
||||
originPlace: Value(originPlace),
|
||||
abundance: Value(abundance),
|
||||
preservationFormat: Value(preservationFormat),
|
||||
updatedAt: Value(updated),
|
||||
lastAuthor: Value(nodeId),
|
||||
),
|
||||
|
|
@ -829,6 +1182,35 @@ class VarietyRepository {
|
|||
return id;
|
||||
}
|
||||
|
||||
/// Records a storage-condition check for a lot (container count + drying-agent
|
||||
/// state). Returns the new check id.
|
||||
Future<String> addConditionCheck({
|
||||
required String lotId,
|
||||
int? checkedOn,
|
||||
int? containerCount,
|
||||
DesiccantState? desiccantState,
|
||||
String? notes,
|
||||
}) async {
|
||||
final (created, updated) = _stamp();
|
||||
final id = idGen.newId();
|
||||
await _db
|
||||
.into(_db.conditionChecks)
|
||||
.insert(
|
||||
ConditionChecksCompanion.insert(
|
||||
id: id,
|
||||
lotId: lotId,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
checkedOn: Value(checkedOn),
|
||||
containerCount: Value(containerCount),
|
||||
desiccantState: Value(desiccantState),
|
||||
notes: Value(notes),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Snapshots the live inventory (tombstones excluded) for the interchange
|
||||
/// export — data-model §7. Includes photo bytes; the JSON codec embeds them
|
||||
/// as base64 and the CSV codec ignores them.
|
||||
|
|
@ -854,6 +1236,9 @@ class VarietyRepository {
|
|||
germinationTests: await (_db.select(
|
||||
_db.germinationTests,
|
||||
)..where((g) => g.isDeleted.equals(false))).get(),
|
||||
conditionChecks: await (_db.select(
|
||||
_db.conditionChecks,
|
||||
)..where((c) => c.isDeleted.equals(false))).get(),
|
||||
movements: await _db.select(_db.movements).get(),
|
||||
parties: await (_db.select(
|
||||
_db.parties,
|
||||
|
|
@ -928,6 +1313,14 @@ class VarietyRepository {
|
|||
updatedAtOf: (r) => r.updatedAt,
|
||||
reconciler: reconciler,
|
||||
);
|
||||
summary += await _importMutableRows(
|
||||
table: _db.conditionChecks,
|
||||
idColumn: _db.conditionChecks.id,
|
||||
rows: snapshot.conditionChecks,
|
||||
idOf: (r) => r.id,
|
||||
updatedAtOf: (r) => r.updatedAt,
|
||||
reconciler: reconciler,
|
||||
);
|
||||
summary += await _importMutableRows(
|
||||
table: _db.parties,
|
||||
idColumn: _db.parties.id,
|
||||
|
|
@ -953,6 +1346,7 @@ class VarietyRepository {
|
|||
for (final n in snapshot.vernacularNames) n.updatedAt,
|
||||
for (final e in snapshot.externalLinks) e.updatedAt,
|
||||
for (final g in snapshot.germinationTests) g.updatedAt,
|
||||
for (final c in snapshot.conditionChecks) c.updatedAt,
|
||||
for (final p in snapshot.parties) p.updatedAt,
|
||||
for (final a in snapshot.attachments) a.updatedAt,
|
||||
]);
|
||||
|
|
@ -990,6 +1384,12 @@ class VarietyRepository {
|
|||
cultivarName: Value(v.cultivarName),
|
||||
notes: Value(v.notes),
|
||||
speciesId: Value(speciesId),
|
||||
needsReproduction: Value(v.needsReproduction),
|
||||
sowMonths: Value(v.sowMonths),
|
||||
transplantMonths: Value(v.transplantMonths),
|
||||
floweringMonths: Value(v.floweringMonths),
|
||||
fruitingMonths: Value(v.fruitingMonths),
|
||||
seedHarvestMonths: Value(v.seedHarvestMonths),
|
||||
),
|
||||
);
|
||||
inserted++;
|
||||
|
|
@ -1014,6 +1414,10 @@ class VarietyRepository {
|
|||
presentation: Value(lot.presentation),
|
||||
storageLocation: Value(lot.storageLocation),
|
||||
offerStatus: Value(lot.offerStatus),
|
||||
originName: Value(lot.originName),
|
||||
originPlace: Value(lot.originPlace),
|
||||
abundance: Value(lot.abundance),
|
||||
preservationFormat: Value(lot.preservationFormat),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1153,7 +1557,11 @@ class VarietyRepository {
|
|||
return ImportSummary(inserted: inserted, skipped: skipped);
|
||||
}
|
||||
|
||||
VarietyLot _toLot(Lot l, List<GerminationEntry> germinationTests) {
|
||||
VarietyLot _toLot(
|
||||
Lot l,
|
||||
List<GerminationEntry> germinationTests,
|
||||
List<ConditionEntry> conditionChecks,
|
||||
) {
|
||||
final hasQuantity =
|
||||
l.quantityKind != null ||
|
||||
l.quantityPrecise != null ||
|
||||
|
|
@ -1165,7 +1573,12 @@ class VarietyRepository {
|
|||
harvestMonth: l.harvestMonth,
|
||||
presentation: l.presentation,
|
||||
storageLocation: l.storageLocation,
|
||||
originName: l.originName,
|
||||
originPlace: l.originPlace,
|
||||
abundance: l.abundance,
|
||||
preservationFormat: l.preservationFormat,
|
||||
germinationTests: germinationTests,
|
||||
conditionChecks: conditionChecks,
|
||||
quantity: hasQuantity
|
||||
? Quantity(
|
||||
kind: _parseKind(l.quantityKind),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue