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
|
|
@ -39,6 +39,12 @@ class BackupSection extends StatelessWidget {
|
|||
subtitle: Text(t.backup.importJsonSubtitle),
|
||||
onTap: () => _runImport(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.playlist_add_outlined),
|
||||
title: Text(t.backup.importCsv),
|
||||
subtitle: Text(t.backup.importCsvSubtitle),
|
||||
onTap: () => _runImportCsv(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -96,6 +102,42 @@ class BackupSection extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _runImportCsv(BuildContext context) async {
|
||||
final t = context.t;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(t.backup.importCsvConfirmTitle),
|
||||
content: Text(t.backup.importCsvConfirmBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: Text(t.backup.importAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
final summary = await _service.importCsv();
|
||||
_show(
|
||||
messenger,
|
||||
summary == null
|
||||
? t.backup.cancelled
|
||||
: t.backup.importCsvDone(count: summary.inserted),
|
||||
);
|
||||
} on FormatException {
|
||||
_show(messenger, t.backup.importFailed);
|
||||
} on Object {
|
||||
_show(messenger, t.backup.failed);
|
||||
}
|
||||
}
|
||||
|
||||
void _show(ScaffoldMessengerState messenger, String message) {
|
||||
messenger.showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue