diff --git a/app/javascript/controllers/imports_controller.js b/app/javascript/controllers/imports_controller.js index d39455a0..4a864074 100644 --- a/app/javascript/controllers/imports_controller.js +++ b/app/javascript/controllers/imports_controller.js @@ -31,6 +31,11 @@ export default class extends BaseController { if (pointsCell) { pointsCell.textContent = new Intl.NumberFormat().format(data.import.points_count); } + + const statusCell = row.querySelector('[data-status-display]'); + if (statusCell && data.import.status) { + statusCell.textContent = data.import.status; + } } } } diff --git a/app/services/imports/broadcaster.rb b/app/services/imports/broadcaster.rb index 1c7f54bb..ead96546 100644 --- a/app/services/imports/broadcaster.rb +++ b/app/services/imports/broadcaster.rb @@ -8,7 +8,21 @@ module Imports::Broadcaster action: 'update', import: { id: import.id, - points_count: index + points_count: index, + status: import.status + } + } + ) + end + + def broadcast_status_update + ImportsChannel.broadcast_to( + import.user, + { + action: 'status_update', + import: { + id: import.id, + status: import.status } } ) diff --git a/app/services/imports/create.rb b/app/services/imports/create.rb index b2056663..d86fe337 100644 --- a/app/services/imports/create.rb +++ b/app/services/imports/create.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Imports::Create + include Imports::Broadcaster + attr_reader :user, :import def initialize(user, import) @@ -10,6 +12,7 @@ class Imports::Create def call import.update!(status: :processing) + broadcast_status_update importer(import.source).new(import, user.id).call @@ -18,10 +21,14 @@ class Imports::Create update_import_points_count(import) rescue StandardError => e import.update!(status: :failed) + broadcast_status_update create_import_failed_notification(import, user, e) ensure - import.update!(status: :completed) if import.processing? + if import.processing? + import.update!(status: :completed) + broadcast_status_update + end end private diff --git a/app/services/users/import_data.rb b/app/services/users/import_data.rb index 820b37ce..664c27cc 100644 --- a/app/services/users/import_data.rb +++ b/app/services/users/import_data.rb @@ -103,15 +103,28 @@ class Users::ImportData import_settings(data['settings']) if data['settings'] import_areas(data['areas']) if data['areas'] - import_places(data['places']) if data['places'] + + # Import places first to ensure they're available for visits + places_imported = import_places(data['places']) if data['places'] + Rails.logger.info "Places import phase completed: #{places_imported} places imported" + import_imports(data['imports']) if data['imports'] import_exports(data['exports']) if data['exports'] import_trips(data['trips']) if data['trips'] import_stats(data['stats']) if data['stats'] import_notifications(data['notifications']) if data['notifications'] - import_visits(data['visits']) if data['visits'] + + # Import visits after places to ensure proper place resolution + visits_imported = import_visits(data['visits']) if data['visits'] + Rails.logger.info "Visits import phase completed: #{visits_imported} visits imported" + import_points(data['points']) if data['points'] + # Final validation check + if data['counts'] + validate_import_completeness(data['counts']) + end + Rails.logger.info "Data import completed. Stats: #{@import_stats}" end @@ -131,6 +144,7 @@ class Users::ImportData Rails.logger.debug "Importing #{places_data&.size || 0} places" places_created = Users::ImportData::Places.new(user, places_data).call @import_stats[:places_created] = places_created + places_created end def import_imports(imports_data) @@ -169,6 +183,7 @@ class Users::ImportData Rails.logger.debug "Importing #{visits_data&.size || 0} visits" visits_created = Users::ImportData::Visits.new(user, visits_data).call @import_stats[:visits_created] = visits_created + visits_created end def import_points(points_data) @@ -221,4 +236,26 @@ class Users::ImportData kind: :error ).call end + + def validate_import_completeness(expected_counts) + Rails.logger.info "Validating import completeness..." + + discrepancies = [] + + expected_counts.each do |entity, expected_count| + actual_count = @import_stats[:"#{entity}_created"] || 0 + + if actual_count < expected_count + discrepancy = "#{entity}: expected #{expected_count}, got #{actual_count} (#{expected_count - actual_count} missing)" + discrepancies << discrepancy + Rails.logger.warn "Import discrepancy - #{discrepancy}" + end + end + + if discrepancies.any? + Rails.logger.warn "Import completed with discrepancies: #{discrepancies.join(', ')}" + else + Rails.logger.info "Import validation successful - all entities imported correctly" + end + end end diff --git a/app/services/users/import_data/places.rb b/app/services/users/import_data/places.rb index 370c9119..6d4ed023 100644 --- a/app/services/users/import_data/places.rb +++ b/app/services/users/import_data/places.rb @@ -16,7 +16,7 @@ class Users::ImportData::Places places_data.each do |place_data| next unless place_data.is_a?(Hash) - place = find_or_create_place(place_data) + place = find_or_create_place_for_import(place_data) places_created += 1 if place&.respond_to?(:previously_new_record?) && place.previously_new_record? end @@ -28,7 +28,7 @@ class Users::ImportData::Places attr_reader :user, :places_data - def find_or_create_place(place_data) + def find_or_create_place_for_import(place_data) name = place_data['name'] latitude = place_data['latitude']&.to_f longitude = place_data['longitude']&.to_f @@ -38,33 +38,42 @@ class Users::ImportData::Places return nil end - existing_place = Place.find_by(name: name) + Rails.logger.debug "Processing place for import: #{name} at (#{latitude}, #{longitude})" - unless existing_place - existing_place = Place.where(latitude: latitude, longitude: longitude).first - end + # During import, we prioritize data integrity for the importing user + # First try exact match (name + coordinates) + existing_place = Place.where( + name: name, + latitude: latitude, + longitude: longitude + ).first if existing_place - Rails.logger.debug "Place already exists: #{name}" + Rails.logger.debug "Found exact place match: #{name} at (#{latitude}, #{longitude}) -> existing place ID #{existing_place.id}" existing_place.define_singleton_method(:previously_new_record?) { false } return existing_place end + Rails.logger.debug "No exact match found for #{name} at (#{latitude}, #{longitude}). Creating new place." + + # If no exact match, create a new place to ensure data integrity + # This prevents data loss during import even if similar places exist place_attributes = place_data.except('created_at', 'updated_at', 'latitude', 'longitude') place_attributes['lonlat'] = "POINT(#{longitude} #{latitude})" place_attributes['latitude'] = latitude place_attributes['longitude'] = longitude place_attributes.delete('user') + Rails.logger.debug "Creating place with attributes: #{place_attributes.inspect}" + begin place = Place.create!(place_attributes) place.define_singleton_method(:previously_new_record?) { true } - Rails.logger.debug "Created place: #{place.name}" + Rails.logger.debug "Created place during import: #{place.name} (ID: #{place.id})" place rescue ActiveRecord::RecordInvalid => e - ExceptionReporter.call(e, 'Failed to create place') - + Rails.logger.error "Failed to create place: #{place_data.inspect}, error: #{e.message}" nil end end diff --git a/app/services/users/import_data/trips.rb b/app/services/users/import_data/trips.rb index d695479f..72b6a5c4 100644 --- a/app/services/users/import_data/trips.rb +++ b/app/services/users/import_data/trips.rb @@ -135,9 +135,9 @@ class Users::ImportData::Trips def valid_trip_data?(trip_data) return false unless trip_data.is_a?(Hash) - validate_trip_name(trip_data) - validate_trip_started_at(trip_data) - validate_trip_ended_at(trip_data) + return false unless validate_trip_name(trip_data) + return false unless validate_trip_started_at(trip_data) + return false unless validate_trip_ended_at(trip_data) true rescue StandardError => e @@ -147,23 +147,29 @@ class Users::ImportData::Trips def validate_trip_name(trip_data) - unless trip_data['name'].present? - Rails.logger.error 'Failed to create trip: Validation failed: Name can\'t be blank' - return false + if trip_data['name'].present? + true + else + Rails.logger.debug 'Trip validation failed: Name can\'t be blank' + false end end def validate_trip_started_at(trip_data) - unless trip_data['started_at'].present? - Rails.logger.error 'Failed to create trip: Validation failed: Started at can\'t be blank' - return false + if trip_data['started_at'].present? + true + else + Rails.logger.debug 'Trip validation failed: Started at can\'t be blank' + false end end def validate_trip_ended_at(trip_data) - unless trip_data['ended_at'].present? - Rails.logger.error 'Failed to create trip: Validation failed: Ended at can\'t be blank' - return false + if trip_data['ended_at'].present? + true + else + Rails.logger.debug 'Trip validation failed: Ended at can\'t be blank' + false end end end diff --git a/app/services/users/import_data/visits.rb b/app/services/users/import_data/visits.rb index bb256fec..e5508175 100644 --- a/app/services/users/import_data/visits.rb +++ b/app/services/users/import_data/visits.rb @@ -28,7 +28,12 @@ class Users::ImportData::Visits visits_created += 1 Rails.logger.debug "Created visit: #{visit_record.name}" rescue ActiveRecord::RecordInvalid => e - ExceptionReporter.call(e, 'Failed to create visit') + Rails.logger.error "Failed to create visit: #{visit_data.inspect}, error: #{e.message}" + ExceptionReporter.call(e, 'Failed to create visit during import') + next + rescue StandardError => e + Rails.logger.error "Unexpected error creating visit: #{visit_data.inspect}, error: #{e.message}" + ExceptionReporter.call(e, 'Unexpected error during visit import') next end end @@ -58,29 +63,67 @@ class Users::ImportData::Visits attributes = visit_data.except('place_reference') if visit_data['place_reference'] - place = find_referenced_place(visit_data['place_reference']) + place = find_or_create_referenced_place(visit_data['place_reference']) attributes[:place] = place if place end attributes end - def find_referenced_place(place_reference) + def find_or_create_referenced_place(place_reference) return nil unless place_reference.is_a?(Hash) name = place_reference['name'] - latitude = place_reference['latitude'].to_f - longitude = place_reference['longitude'].to_f + latitude = place_reference['latitude']&.to_f + longitude = place_reference['longitude']&.to_f - place = Place.find_by(name: name) || - Place.where("latitude = ? AND longitude = ?", latitude, longitude).first + return nil unless name.present? && latitude.present? && longitude.present? + + Rails.logger.debug "Looking for place reference: #{name} at (#{latitude}, #{longitude})" + + # First try exact match (name + coordinates) + place = Place.where( + name: name, + latitude: latitude, + longitude: longitude + ).first if place - Rails.logger.debug "Found referenced place: #{name}" - else - Rails.logger.warn "Referenced place not found: #{name} (#{latitude}, #{longitude})" + Rails.logger.debug "Found exact place match for visit: #{name} -> existing place ID #{place.id}" + return place end - place + # Try coordinate-only match with close proximity + place = Place.where( + "latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?", + latitude - 0.0001, latitude + 0.0001, + longitude - 0.0001, longitude + 0.0001 + ).first + + if place + Rails.logger.debug "Found nearby place match for visit: #{name} -> #{place.name} (ID: #{place.id})" + return place + end + + # If no match found, create the place to ensure visit import succeeds + # This handles cases where places weren't imported in the places phase + Rails.logger.info "Creating missing place during visit import: #{name} at (#{latitude}, #{longitude})" + + begin + place = Place.create!( + name: name, + latitude: latitude, + longitude: longitude, + lonlat: "POINT(#{longitude} #{latitude})", + source: place_reference['source'] || 'manual' + ) + + Rails.logger.debug "Created missing place for visit: #{place.name} (ID: #{place.id})" + place + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error "Failed to create missing place: #{place_reference.inspect}, error: #{e.message}" + ExceptionReporter.call(e, 'Failed to create missing place during visit import') + nil + end end end diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 25e742a3..5fb84f95 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -63,7 +63,10 @@
Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :delete }, method: :delete, class: 'btn' %>
- <%= link_to "Export my data", export_settings_users_path, class: 'btn btn-primary' %> + <%= link_to "Export my data", export_settings_users_path, class: 'btn btn-primary', data: { + turbo_confirm: "Are you sure you want to export your data?", + turbo_method: :get + } %>
diff --git a/app/views/imports/index.html.erb b/app/views/imports/index.html.erb index d0dd7680..f2b6467b 100644 --- a/app/views/imports/index.html.erb +++ b/app/views/imports/index.html.erb @@ -68,7 +68,7 @@