feat(inventory): additive CSV import for bulk digitization
Import a spreadsheet list (the same 21-column schema the CSV export already produces) as new varieties/lots. Tolerant reader: columns matched by header name in any order, only variety_label required, unknown enums fall back to a safe default, rows sharing variety_id collapse into one variety with several lots. Species re-linked by scientific name. Additive by design (fresh ids + HLC), so it never overwrites existing rows; JSON stays the canonical id-reconciling import. Adds a hand-rolled RFC 4180 parser (no new dependency), an importCsv path in VarietyRepository/ExportImportService, an 'Import from CSV' tile in the backup section, and en/es strings. Note: i18n .g.dart also carries pre-existing About/intro strings from the working tree (regenerated by slang).
This commit is contained in:
parent
21072633b5
commit
89b61bc9e4
11 changed files with 1070 additions and 2 deletions
|
|
@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart';
|
|||
import '../db/database.dart';
|
||||
import '../db/enums.dart';
|
||||
import 'export_import/import_reconciler.dart';
|
||||
import 'export_import/inventory_csv_codec.dart';
|
||||
import 'export_import/inventory_snapshot.dart';
|
||||
|
||||
/// A lightweight row for the inventory list (only what the list renders).
|
||||
|
|
@ -961,6 +962,113 @@ class VarietyRepository {
|
|||
return summary;
|
||||
}
|
||||
|
||||
/// Additive import of a spreadsheet [csv]: every parsed variety is inserted
|
||||
/// fresh (new UUIDv7 + local HLC), so it never overwrites existing rows and
|
||||
/// re-importing the same file adds it again. The canonical, id-reconciling
|
||||
/// import is JSON ([importInventory]); this is the low-friction path for a
|
||||
/// list a collective already keeps in a spreadsheet.
|
||||
///
|
||||
/// Species are re-linked by scientific name against the local catalog
|
||||
/// (unmatched → null). Returns the number of varieties added.
|
||||
Future<ImportSummary> importCsv(CsvImport csv) async {
|
||||
var inserted = 0;
|
||||
await _db.transaction(() async {
|
||||
for (final v in csv.varieties) {
|
||||
final speciesId = await _resolveSpeciesByName(v.scientificName);
|
||||
final varietyId = idGen.newId();
|
||||
final (created, updated) = _stamp();
|
||||
await _db
|
||||
.into(_db.varieties)
|
||||
.insert(
|
||||
VarietiesCompanion.insert(
|
||||
id: varietyId,
|
||||
label: v.label,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
category: Value(v.category),
|
||||
cultivarName: Value(v.cultivarName),
|
||||
notes: Value(v.notes),
|
||||
speciesId: Value(speciesId),
|
||||
),
|
||||
);
|
||||
inserted++;
|
||||
|
||||
for (final lot in v.lots) {
|
||||
final (created, updated) = _stamp();
|
||||
await _db
|
||||
.into(_db.lots)
|
||||
.insert(
|
||||
LotsCompanion.insert(
|
||||
id: idGen.newId(),
|
||||
varietyId: varietyId,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
type: Value(lot.type),
|
||||
harvestYear: Value(lot.harvestYear),
|
||||
harvestMonth: Value(lot.harvestMonth),
|
||||
quantityKind: Value(lot.quantityKind),
|
||||
quantityPrecise: Value(lot.quantityPrecise),
|
||||
quantityLabel: Value(lot.quantityLabel),
|
||||
presentation: Value(lot.presentation),
|
||||
storageLocation: Value(lot.storageLocation),
|
||||
offerStatus: Value(lot.offerStatus),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final name in v.vernacularNames) {
|
||||
final (created, updated) = _stamp();
|
||||
await _db
|
||||
.into(_db.varietyVernacularNames)
|
||||
.insert(
|
||||
VarietyVernacularNamesCompanion.insert(
|
||||
id: idGen.newId(),
|
||||
varietyId: varietyId,
|
||||
name: name.name,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
language: Value(name.language),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final link in v.links) {
|
||||
final (created, updated) = _stamp();
|
||||
await _db
|
||||
.into(_db.externalLinks)
|
||||
.insert(
|
||||
ExternalLinksCompanion.insert(
|
||||
id: idGen.newId(),
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
parentType: ParentType.variety,
|
||||
parentId: varietyId,
|
||||
url: link.url,
|
||||
title: Value(link.title),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return ImportSummary(inserted: inserted);
|
||||
}
|
||||
|
||||
/// Resolves a single scientific name to a local catalog species id, or null
|
||||
/// when blank or unmatched.
|
||||
Future<String?> _resolveSpeciesByName(String? scientificName) async {
|
||||
final name = scientificName?.trim();
|
||||
if (name == null || name.isEmpty) return null;
|
||||
final local = await (_db.select(_db.species)..where(
|
||||
(s) => s.scientificName.equals(name) & s.isDeleted.equals(false),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
return local?.id;
|
||||
}
|
||||
|
||||
/// Maps incoming species ids to local catalog ids by scientific name.
|
||||
Future<Map<String, String?>> _resolveSpecies(
|
||||
Map<String, String> speciesNamesById,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue