Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class UddfImportSelections {
final Set<int> tags;
final Set<int> diveTypes;
final Set<int> sites;

/// Maps import-list index → existing site ID for sites the user chose to
/// overwrite. These indices are NOT included in [sites] (which creates new
/// entries); instead, the matching existing site is updated in place.
final Map<int, String> siteOverrides;
final Set<int> equipmentSets;
final Set<int> dives;
final Set<int> courses;
Expand All @@ -92,6 +97,7 @@ class UddfImportSelections {
this.tags = const {},
this.diveTypes = const {},
this.sites = const {},
this.siteOverrides = const {},
this.equipmentSets = const {},
this.dives = const {},
this.courses = const {},
Expand Down Expand Up @@ -313,6 +319,7 @@ class UddfEntityImporter {
final sitesCount = await _importSites(
data.sites,
selections.sites,
selections.siteOverrides,
repositories.siteRepository,
diverId,
siteIdMapping,
Expand Down Expand Up @@ -730,6 +737,7 @@ class UddfEntityImporter {
Future<int> _importSites(
List<Map<String, dynamic>> items,
Set<int> selected,
Map<int, String> overrides,
SiteRepository repository,
String diverId,
Map<String, DiveSite> idMapping,
Expand All @@ -740,11 +748,14 @@ class UddfEntityImporter {
// dives referencing them still get linked correctly.
final existingSites = await repository.getAllSites(diverId: diverId);
final existingByName = <String, DiveSite>{};
final existingById = <String, DiveSite>{};
for (final site in existingSites) {
existingByName[site.name.toLowerCase()] = site;
existingById[site.id] = site;
}
for (var i = 0; i < items.length; i++) {
if (selected.contains(i)) continue; // will be imported below
if (overrides.containsKey(i)) continue; // will be overwritten below
final uddfId = items[i]['uddfId'] as String?;
final name = items[i]['name'] as String?;
if (uddfId != null && name != null) {
Expand All @@ -755,10 +766,91 @@ class UddfEntityImporter {
}
}

if (selected.isEmpty) return 0;
onProgress?.call(ImportPhase.sites, 0, selected.length);
final totalWork = selected.length + overrides.length;
if (totalWork == 0) return 0;
onProgress?.call(ImportPhase.sites, 0, totalWork);
var count = 0;

// Handle overwrite (replaceSource): update existing sites in place.
for (final entry in overrides.entries) {
final i = entry.key;
final existingId = entry.value;
if (i >= items.length) continue;
final siteData = items[i];
final name = siteData['name'] as String?;
if (name == null || name.isEmpty) continue;

final existing = existingById[existingId];
if (existing == null) continue;

final uddfId = siteData['uddfId'] as String?;
final lat = siteData['latitude'] as double?;
final lon = siteData['longitude'] as double?;

String? country = siteData['country'] as String?;
String? region = siteData['region'] as String?;

if (lat != null && lon != null && (country == null || region == null)) {
try {
final geocodeResult = await LocationService.instance.reverseGeocode(
lat,
lon,
);
country ??= geocodeResult.country;
region ??= geocodeResult.region;
} catch (_) {
// Geocoding is best-effort
}
}

final difficultyStr = siteData['difficulty'] as String?;
final difficulty = difficultyStr != null
? SiteDifficulty.fromString(difficultyStr)
: null;

final overwrittenSite = DiveSite(
id: existingId,
diverId: diverId,
name: name,
description: siteData['description'] as String? ?? '',
location: (lat != null && lon != null) ? GeoPoint(lat, lon) : null,
minDepth: siteData['minDepth'] as double?,
maxDepth: siteData['maxDepth'] as double?,
difficulty: difficulty,
country: country,
region: region,
rating: siteData['rating'] as double?,
notes: siteData['notes'] as String? ?? '',
hazards: siteData['hazards'] as String?,
accessNotes: siteData['accessNotes'] as String?,
mooringNumber: siteData['mooringNumber'] as String?,
parkingInfo: siteData['parkingInfo'] as String?,
altitude: siteData['altitude'] as double?,
);
Comment on lines +811 to +829

await repository.updateSite(overwrittenSite);

final waterType = siteData['waterType'] as String?;
final bodyOfWater = siteData['bodyOfWater'] as String?;
if (waterType != null || bodyOfWater != null) {
await repository.applyImportedMetadata(
existingId,
DiveSitesCompanion(
waterType: waterType != null
? Value(waterType)
: const Value.absent(),
bodyOfWater: bodyOfWater != null
? Value(bodyOfWater)
: const Value.absent(),
),
);
}

if (uddfId != null) idMapping[uddfId] = overwrittenSite;
count++;
onProgress?.call(ImportPhase.sites, count, totalWork);
}
Comment on lines +774 to +852

for (var i = 0; i < items.length; i++) {
if (!selected.contains(i)) continue;
final siteData = items[i];
Expand Down Expand Up @@ -834,7 +926,7 @@ class UddfEntityImporter {

if (uddfId != null) idMapping[uddfId] = createdSite;
count++;
onProgress?.call(ImportPhase.sites, count, selected.length);
onProgress?.call(ImportPhase.sites, count, totalWork);
}

return count;
Expand Down
23 changes: 23 additions & 0 deletions lib/features/import_wizard/data/adapters/universal_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class UniversalAdapter implements ImportSourceAdapter {
Set<DuplicateAction> get supportedDuplicateActions => const {
DuplicateAction.skip,
DuplicateAction.importAsNew,
DuplicateAction.replaceSource,
};
Comment on lines 140 to 144

@override
Expand Down Expand Up @@ -419,6 +420,7 @@ class UniversalAdapter implements ImportSourceAdapter {
final uddfSelections = UddfImportSelections(
dives: resolve(wizard.ImportEntityType.dives),
sites: resolve(wizard.ImportEntityType.sites),
siteOverrides: _resolveSiteOverrides(duplicateActions, bundle),
buddies: resolve(wizard.ImportEntityType.buddies),
equipment: resolve(wizard.ImportEntityType.equipment),
trips: resolve(wizard.ImportEntityType.trips),
Expand Down Expand Up @@ -692,6 +694,27 @@ class UniversalAdapter implements ImportSourceAdapter {
// Helpers — import
// ---------------------------------------------------------------------------

/// Build a map of import-list index → existing site ID for sites the user
/// chose to overwrite ([DuplicateAction.replaceSource]).
Map<int, String> _resolveSiteOverrides(
Map<wizard.ImportEntityType, Map<int, DuplicateAction>> duplicateActions,
ImportBundle bundle,
) {
final actions = duplicateActions[wizard.ImportEntityType.sites] ?? const {};
final entityMatches =
bundle.groups[wizard.ImportEntityType.sites]?.entityMatches ?? const {};
final overrides = <int, String>{};
for (final entry in actions.entries) {
if (entry.value == DuplicateAction.replaceSource) {
final existingId = entityMatches[entry.key]?.existingId;
if (existingId != null) {
overrides[entry.key] = existingId;
}
}
}
return overrides;
}

/// Resolve the final selection set for [type] by merging the base
/// selections with duplicate actions. Duplicate items whose action is
/// [DuplicateAction.importAsNew] are added; items in the base set whose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,14 +494,12 @@ class _EntityDuplicateCardState extends State<_EntityDuplicateCard> {
// the tertiary warning colour so the border reads as "undecided" rather
// than implying a skip. The pending branch below also uses tertiary, so
// this fallback only matters for the rare non-pending-null case.
final Color borderColor;
if (widget.selectedAction == null) {
borderColor = colorScheme.tertiary;
} else if (isImporting) {
borderColor = Colors.green;
} else {
borderColor = colorScheme.error;
}
final Color borderColor = switch (widget.selectedAction) {
null => colorScheme.tertiary,
DuplicateAction.importAsNew => Colors.green,
DuplicateAction.replaceSource => Colors.blue.shade700,
_ => colorScheme.error,
};

final BorderSide borderSide = widget.isPending
? BorderSide(color: colorScheme.tertiary, width: 1.5)
Expand Down Expand Up @@ -580,7 +578,7 @@ class _EntityDuplicateCardState extends State<_EntityDuplicateCard> {
// Action badge — suppressed when no decision has been made.
if (widget.selectedAction != null) ...[
const SizedBox(width: 8),
_SimpleActionBadge(isImporting: isImporting),
_SimpleActionBadge(action: widget.selectedAction!),
],
// Expand/collapse chevron (only when comparison data exists)
if (widget.entityMatch != null) ...[
Expand Down Expand Up @@ -723,6 +721,14 @@ class _EntityComparisonPanel extends StatelessWidget {
onPressed: () =>
onActionChanged(DuplicateAction.importAsNew),
),
_EntityActionButton(
label: 'Overwrite',
subtitle: 'Replace existing entry',
isSelected: selectedAction == DuplicateAction.replaceSource,
color: Colors.blue.shade700,
onPressed: () =>
onActionChanged(DuplicateAction.replaceSource),
),
Comment on lines +724 to +731
],
),
],
Expand Down Expand Up @@ -872,16 +878,18 @@ class _EntityActionButton extends StatelessWidget {
}

class _SimpleActionBadge extends StatelessWidget {
final bool isImporting;
final DuplicateAction action;

const _SimpleActionBadge({required this.isImporting});
const _SimpleActionBadge({required this.action});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final (label, color) = isImporting
? ('IMPORT', Colors.green.shade700)
: ('SKIP', theme.colorScheme.error);
final (label, color) = switch (action) {
DuplicateAction.importAsNew => ('IMPORT', Colors.green.shade700),
DuplicateAction.replaceSource => ('OVERWRITE', Colors.blue.shade700),
_ => ('SKIP', theme.colorScheme.error),
};

return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
Expand Down
Loading