diff --git a/app/controllers/api/v1/collection_artifacts_controller.rb b/app/controllers/api/v1/collection_artifacts_controller.rb index 5197f50..58a6484 100644 --- a/app/controllers/api/v1/collection_artifacts_controller.rb +++ b/app/controllers/api/v1/collection_artifacts_controller.rb @@ -123,7 +123,8 @@ module Api game_data, update_existing: import_params[:update_existing] == true, is_full_inventory: import_params[:is_full_inventory] == true, - reconcile_deletions: import_params[:reconcile_deletions] == true + reconcile_deletions: import_params[:reconcile_deletions] == true, + filter: import_params[:filter] ) result = service.import @@ -147,12 +148,13 @@ module Api # @return [JSON] List of items that would be deleted def preview_sync game_data = import_params[:data] + filter = import_params[:filter] unless game_data.present? return render json: { error: 'No data provided' }, status: :bad_request end - service = ArtifactImportService.new(current_user, game_data) + service = ArtifactImportService.new(current_user, game_data, filter: filter) items_to_delete = service.preview_deletions render json: { @@ -234,7 +236,8 @@ module Api update_existing: params[:update_existing], is_full_inventory: params[:is_full_inventory], reconcile_deletions: params[:reconcile_deletions], - data: params[:data]&.to_unsafe_h + data: params[:data]&.to_unsafe_h, + filter: params[:filter]&.to_unsafe_h } end diff --git a/app/controllers/api/v1/collection_summons_controller.rb b/app/controllers/api/v1/collection_summons_controller.rb index d791a64..57d8ffc 100644 --- a/app/controllers/api/v1/collection_summons_controller.rb +++ b/app/controllers/api/v1/collection_summons_controller.rb @@ -127,7 +127,8 @@ module Api game_data, update_existing: import_params[:update_existing] == true, is_full_inventory: import_params[:is_full_inventory] == true, - reconcile_deletions: import_params[:reconcile_deletions] == true + reconcile_deletions: import_params[:reconcile_deletions] == true, + filter: import_params[:filter] ) result = service.import @@ -151,12 +152,13 @@ module Api # @return [JSON] List of items that would be deleted def preview_sync game_data = import_params[:data] + filter = import_params[:filter] unless game_data.present? return render json: { error: 'No data provided' }, status: :bad_request end - service = SummonImportService.new(current_user, game_data) + service = SummonImportService.new(current_user, game_data, filter: filter) items_to_delete = service.preview_deletions render json: { @@ -218,7 +220,8 @@ module Api update_existing: params[:update_existing], is_full_inventory: params[:is_full_inventory], reconcile_deletions: params[:reconcile_deletions], - data: params[:data]&.to_unsafe_h + data: params[:data]&.to_unsafe_h, + filter: params[:filter]&.to_unsafe_h } end diff --git a/app/controllers/api/v1/collection_weapons_controller.rb b/app/controllers/api/v1/collection_weapons_controller.rb index c22869a..def412a 100644 --- a/app/controllers/api/v1/collection_weapons_controller.rb +++ b/app/controllers/api/v1/collection_weapons_controller.rb @@ -135,7 +135,8 @@ module Api game_data, update_existing: import_params[:update_existing] == true, is_full_inventory: import_params[:is_full_inventory] == true, - reconcile_deletions: import_params[:reconcile_deletions] == true + reconcile_deletions: import_params[:reconcile_deletions] == true, + filter: import_params[:filter] ) result = service.import @@ -159,12 +160,13 @@ module Api # @return [JSON] List of items that would be deleted def preview_sync game_data = import_params[:data] + filter = import_params[:filter] unless game_data.present? return render json: { error: 'No data provided' }, status: :bad_request end - service = WeaponImportService.new(current_user, game_data) + service = WeaponImportService.new(current_user, game_data, filter: filter) items_to_delete = service.preview_deletions render json: { @@ -236,7 +238,8 @@ module Api update_existing: params[:update_existing], is_full_inventory: params[:is_full_inventory], reconcile_deletions: params[:reconcile_deletions], - data: params[:data]&.to_unsafe_h + data: params[:data]&.to_unsafe_h, + filter: params[:filter]&.to_unsafe_h } end diff --git a/app/services/artifact_import_service.rb b/app/services/artifact_import_service.rb index 1397e26..f3b1a78 100644 --- a/app/services/artifact_import_service.rb +++ b/app/services/artifact_import_service.rb @@ -32,6 +32,7 @@ class ArtifactImportService @update_existing = options[:update_existing] || false @is_full_inventory = options[:is_full_inventory] || false @reconcile_deletions = options[:reconcile_deletions] || false + @filter = options[:filter] # { elements: [...], proficiencies: [...] } @created = [] @updated = [] @skipped = [] @@ -42,6 +43,7 @@ class ArtifactImportService ## # Previews what would be deleted in a sync operation. # Does not modify any data, just returns items that would be removed. + # When a filter is active, only considers items matching that filter. # # @return [Array] Collection artifacts that would be deleted def preview_deletions @@ -58,10 +60,14 @@ class ArtifactImportService return [] if game_ids.empty? # Find collection artifacts with game_ids NOT in the import - @user.collection_artifacts - .includes(:artifact) - .where.not(game_id: nil) - .where.not(game_id: game_ids) + # Scoped to filter criteria if present + scope = @user.collection_artifacts + .includes(:artifact) + .where.not(game_id: nil) + .where.not(game_id: game_ids) + + scope = apply_filter_scope(scope) + scope end ## @@ -243,18 +249,22 @@ class ArtifactImportService ## # Reconciles deletions by removing collection artifacts not in the processed list. # Only called when @is_full_inventory and @reconcile_deletions are both true. + # When a filter is active, only deletes items matching that filter. # # @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs def reconcile_deletions # Find collection artifacts with game_ids NOT in our processed list - missing = @user.collection_artifacts - .where.not(game_id: nil) - .where.not(game_id: @processed_game_ids) + # Scoped to filter criteria if present + scope = @user.collection_artifacts + .where.not(game_id: nil) + .where.not(game_id: @processed_game_ids) + + scope = apply_filter_scope(scope) deleted_count = 0 orphaned_grid_item_ids = [] - missing.find_each do |coll_artifact| + scope.find_each do |coll_artifact| # Collect IDs of grid items that will be orphaned grid_artifact_ids = GridArtifact.where(collection_artifact_id: coll_artifact.id).pluck(:id) orphaned_grid_item_ids.concat(grid_artifact_ids) @@ -269,4 +279,28 @@ class ArtifactImportService orphaned_grid_items: orphaned_grid_item_ids } end + + ## + # Applies element and proficiency filters to a collection artifacts scope. + # Used to scope deletion checks to only items matching the current game filter. + # + # @param scope [ActiveRecord::Relation] The collection artifacts relation to filter + # @return [ActiveRecord::Relation] Filtered relation + def apply_filter_scope(scope) + return scope unless @filter.present? + + # Filter by elements if specified + if @filter[:elements].present? || @filter['elements'].present? + elements = @filter[:elements] || @filter['elements'] + scope = scope.where(element: elements) + end + + # Filter by proficiencies if specified + if @filter[:proficiencies].present? || @filter['proficiencies'].present? + proficiencies = @filter[:proficiencies] || @filter['proficiencies'] + scope = scope.where(proficiency: proficiencies) + end + + scope + end end diff --git a/app/services/summon_import_service.rb b/app/services/summon_import_service.rb index 3e5c3a8..b4737ff 100644 --- a/app/services/summon_import_service.rb +++ b/app/services/summon_import_service.rb @@ -20,6 +20,7 @@ class SummonImportService @update_existing = options[:update_existing] || false @is_full_inventory = options[:is_full_inventory] || false @reconcile_deletions = options[:reconcile_deletions] || false + @filter = options[:filter] # { elements: [...] } @created = [] @updated = [] @skipped = [] @@ -30,6 +31,7 @@ class SummonImportService ## # Previews what would be deleted in a sync operation. # Does not modify any data, just returns items that would be removed. + # When a filter is active, only considers items matching that filter. # # @return [Array] Collection summons that would be deleted def preview_deletions @@ -45,10 +47,14 @@ class SummonImportService return [] if game_ids.empty? # Find collection summons with game_ids NOT in the import - @user.collection_summons - .includes(:summon) - .where.not(game_id: nil) - .where.not(game_id: game_ids) + # Scoped to filter criteria if present + scope = @user.collection_summons + .includes(:summon) + .where.not(game_id: nil) + .where.not(game_id: game_ids) + + scope = apply_filter_scope(scope) + scope end ## @@ -193,18 +199,22 @@ class SummonImportService ## # Reconciles deletions by removing collection summons not in the processed list. # Only called when @is_full_inventory and @reconcile_deletions are both true. + # When a filter is active, only deletes items matching that filter. # # @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs def reconcile_deletions # Find collection summons with game_ids NOT in our processed list - missing = @user.collection_summons - .where.not(game_id: nil) - .where.not(game_id: @processed_game_ids) + # Scoped to filter criteria if present + scope = @user.collection_summons + .where.not(game_id: nil) + .where.not(game_id: @processed_game_ids) + + scope = apply_filter_scope(scope) deleted_count = 0 orphaned_grid_item_ids = [] - missing.find_each do |coll_summon| + scope.find_each do |coll_summon| # Collect IDs of grid items that will be orphaned grid_summon_ids = GridSummon.where(collection_summon_id: coll_summon.id).pluck(:id) orphaned_grid_item_ids.concat(grid_summon_ids) @@ -219,4 +229,23 @@ class SummonImportService orphaned_grid_items: orphaned_grid_item_ids } end + + ## + # Applies element filter to a collection summons scope. + # Used to scope deletion checks to only items matching the current game filter. + # + # @param scope [ActiveRecord::Relation] The collection summons relation to filter + # @return [ActiveRecord::Relation] Filtered relation + def apply_filter_scope(scope) + return scope unless @filter.present? + + # Element: always join through summon (no element on collection_summons) + if @filter[:elements].present? || @filter['elements'].present? + elements = @filter[:elements] || @filter['elements'] + scope = scope.joins(:summon).where(summons: { element: elements }) + end + + # Summons don't have proficiency - ignore if present in filter + scope + end end diff --git a/app/services/weapon_import_service.rb b/app/services/weapon_import_service.rb index 1522531..b693194 100644 --- a/app/services/weapon_import_service.rb +++ b/app/services/weapon_import_service.rb @@ -30,6 +30,7 @@ class WeaponImportService @update_existing = options[:update_existing] || false @is_full_inventory = options[:is_full_inventory] || false @reconcile_deletions = options[:reconcile_deletions] || false + @filter = options[:filter] # { elements: [...], proficiencies: [...] } @created = [] @updated = [] @skipped = [] @@ -42,6 +43,7 @@ class WeaponImportService ## # Previews what would be deleted in a sync operation. # Does not modify any data, just returns items that would be removed. + # When a filter is active, only considers items matching that filter. # # @return [Array] Collection weapons that would be deleted def preview_deletions @@ -57,10 +59,14 @@ class WeaponImportService return [] if game_ids.empty? # Find collection weapons with game_ids NOT in the import - @user.collection_weapons - .includes(:weapon) - .where.not(game_id: nil) - .where.not(game_id: game_ids) + # Scoped to filter criteria if present + scope = @user.collection_weapons + .includes(:weapon) + .where.not(game_id: nil) + .where.not(game_id: game_ids) + + scope = apply_filter_scope(scope) + scope end ## @@ -357,18 +363,22 @@ class WeaponImportService ## # Reconciles deletions by removing collection weapons not in the processed list. # Only called when @is_full_inventory and @reconcile_deletions are both true. + # When a filter is active, only deletes items matching that filter. # # @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs def reconcile_deletions # Find collection weapons with game_ids NOT in our processed list - missing = @user.collection_weapons - .where.not(game_id: nil) - .where.not(game_id: @processed_game_ids) + # Scoped to filter criteria if present + scope = @user.collection_weapons + .where.not(game_id: nil) + .where.not(game_id: @processed_game_ids) + + scope = apply_filter_scope(scope) deleted_count = 0 orphaned_grid_item_ids = [] - missing.find_each do |coll_weapon| + scope.find_each do |coll_weapon| # Collect IDs of grid items that will be orphaned grid_weapon_ids = GridWeapon.where(collection_weapon_id: coll_weapon.id).pluck(:id) orphaned_grid_item_ids.concat(grid_weapon_ids) @@ -383,4 +393,32 @@ class WeaponImportService orphaned_grid_items: orphaned_grid_item_ids } end + + ## + # Applies element and proficiency filters to a collection weapons scope. + # Used to scope deletion checks to only items matching the current game filter. + # + # @param scope [ActiveRecord::Relation] The collection weapons relation to filter + # @return [ActiveRecord::Relation] Filtered relation + def apply_filter_scope(scope) + return scope unless @filter.present? + + # Element: check collection_weapon.element first (for element-changeable weapons), + # fall back to weapon.element if nil + if @filter[:elements].present? || @filter['elements'].present? + elements = @filter[:elements] || @filter['elements'] + scope = scope.joins(:weapon).where( + 'collection_weapons.element IN (?) OR (collection_weapons.element IS NULL AND weapons.element IN (?))', + elements, elements + ) + end + + # Proficiency: join through weapon + if @filter[:proficiencies].present? || @filter['proficiencies'].present? + proficiencies = @filter[:proficiencies] || @filter['proficiencies'] + scope = scope.joins(:weapon).where(weapons: { proficiency: proficiencies }) + end + + scope + end end