From 774bb9d75997d45f2d712fea80b2075621ee31f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 11:16:54 +0200 Subject: [PATCH 01/24] Changelog V106.0.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95163236a..982399841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Dev +## V106.0.0 ### Added - Destinations: Add a duration by destination [#361](https://github.com/cartoway/planner-web/pull/361) - Visits From a4f4515b00162b39121a23eadd3d170f36917ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 11:17:49 +0200 Subject: [PATCH 02/24] Changelog dev --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 982399841..b83550d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Dev + ### Added + + ### Changed + + ### Removed + + ### Fixed + ## V106.0.0 ### Added - Destinations: Add a duration by destination [#361](https://github.com/cartoway/planner-web/pull/361) From 262c71d1c8b83a580350cd6884d58a79ea6c6dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Mon, 30 Jun 2025 09:07:18 +0200 Subject: [PATCH 03/24] Store solver & skipped solvers --- app/jobs/optimizer_job.rb | 2 +- lib/optim/optimizer_wrapper.rb | 22 +++-- .../vrp-with-solvers-info.json | 24 +++++ test/lib/optim/optimizer_wrapper_test.rb | 93 +++++++++++++++++++ 4 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/optimizer-wrapper/vrp-with-solvers-info.json diff --git a/app/jobs/optimizer_job.rb b/app/jobs/optimizer_job.rb index 509be2378..067aa31a9 100644 --- a/app/jobs/optimizer_job.rb +++ b/app/jobs/optimizer_job.rb @@ -45,7 +45,7 @@ def perform options = job_options(planning).merge(options) optimum = Planner::Application.config.optimizer.optimize(planning, routes, **options) { |job_id, solution_data| if @job - job_progress_save solution_data.merge('job_id': job_id, 'completed': false) + job_progress_save solution_data.merge(job_id: job_id, completed: false) Delayed::Worker.logger.info("OptimizerJob", customer_id: customer_id, planning_id: planning_id, progress: @job.progress) end } diff --git a/lib/optim/optimizer_wrapper.rb b/lib/optim/optimizer_wrapper.rb index 2d08d74df..345ca0775 100644 --- a/lib/optim/optimizer_wrapper.rb +++ b/lib/optim/optimizer_wrapper.rb @@ -125,7 +125,7 @@ def solve(vrp, progress, key) end response } - + solution_data = {} result = nil while json retry_counter = 0 @@ -138,7 +138,7 @@ def solve(vrp, progress, key) elsif ['queued', 'working'].include?(result.dig('job', 'status')) begin if progress && job_details - solution_data = compute_progression(vrp, result, job_details) + solution_data.merge!(compute_progression(vrp, result, job_details)) progress.call(job_id, solution_data) end sleep(0.2) @@ -468,9 +468,10 @@ def insertion_only_vehicles(routes, vrp) # If the problem is simple, the progression is directly related to the time elapsed as there is only one matrix to compute def compute_progression(vrp, result, job_details) progression = job_details.dig('avancement') - return {'first_progression': 0, 'second_progression': 0, 'status': 'queued'} unless progression + solution_data = {first_progression: 0, second_progression: 0, status: 'queued'} + solution_data.merge!(compute_solution_data(result.dig('job'), result.dig('solutions')&.last)) - solution_data = compute_solution_data(result.dig('job'), result.dig('solutions')&.last) + return solution_data unless progression multipart, matrix_bar, resolution_bar = if PROGRESSION_KEYS.any?{ |key| progression.include?(key) } @@ -480,7 +481,7 @@ def compute_progression(vrp, result, job_details) else [nil, 0, 0] end - solution_data.merge!('multipart': multipart, 'first_progression': matrix_bar, 'second_progression': resolution_bar) + solution_data.merge!(multipart: multipart, first_progression: matrix_bar, second_progression: resolution_bar) solution_data end @@ -529,8 +530,13 @@ def parse_progression(progression, key) def compute_solution_data(job, solution) solution_data = solution&.slice('cost', 'total_distance', 'total_time', 'elapsed') || {} solution_data.merge!(job.slice('status')) - solution_data.merge('status': 'queued') unless solution_data.key?('status') - solution_data.merge!('unassigned_size': solution.dig('unassigned')&.size) if solution - solution_data + solution_data.merge!('unassigned_size' => solution.dig('unassigned')&.size) if solution + + # Add solver information + solution_data.merge!('solvers' => job.dig('solvers')) if job.dig('solvers') + solution_data.merge!('skipped_services' => job.dig('skipped_services')) if job.dig('skipped_services') + + # Symbolize all keys for consistency + solution_data.symbolize_keys end end diff --git a/test/fixtures/optimizer-wrapper/vrp-with-solvers-info.json b/test/fixtures/optimizer-wrapper/vrp-with-solvers-info.json new file mode 100644 index 000000000..6860a40fc --- /dev/null +++ b/test/fixtures/optimizer-wrapper/vrp-with-solvers-info.json @@ -0,0 +1,24 @@ +{ + "solutions": null, + "geojsons": null, + "job": { + "id": "477952a5d1c4a7cc2bff3188232659b9", + "status": "queued", + "avancement": null, + "graph": null, + "solvers": [ + "ortools" + ], + "skipped_services": [ + { + "solver": "vroom", + "reasons": [ + "assert_no_relations_except_simple_shipments", + "assert_vehicles_no_duration_limit", + "assert_no_complex_setup_durations", + "assert_no_first_solution_strategy" + ] + } + ] + } +} diff --git a/test/lib/optim/optimizer_wrapper_test.rb b/test/lib/optim/optimizer_wrapper_test.rb index 3169e742e..ee3871faf 100644 --- a/test/lib/optim/optimizer_wrapper_test.rb +++ b/test/lib/optim/optimizer_wrapper_test.rb @@ -339,4 +339,97 @@ class OptimizerWrapperTest < ActionController::TestCase vrp = @optim.build_vrp(@planning, @planning.routes, **{ cost_fixed: 10 }) assert_equal 10, vrp[:vehicles].first[:cost_fixed] end + + test 'should include solver information in progress data' do + uri_template_post = Addressable::Template.new('http://localhost:1791/0.1/vrp/submit.json') + uri_template = Addressable::Template.new('http://localhost:1791/0.1/vrp/jobs/{job_id}.json?api_key={api_key}') + + # Test with solver information + progress_count = 0 + progress = lambda{ |job_id, solution_data| + case progress_count + when 1 + assert_equal 'queued', solution_data[:status] + when 2 + assert_equal 'completed', solution_data[:status] + end + if solution_data && solution_data[:status] == 'queued' + assert_equal ['ortools'], solution_data[:solvers] + assert_equal 1, solution_data[:skipped_services].size + assert_equal 'vroom', solution_data[:skipped_services][0]['solver'] + assert_equal ['assert_no_relations_except_simple_shipments', 'assert_vehicles_no_duration_limit', 'assert_no_complex_setup_durations', 'assert_no_first_solution_strategy'], solution_data[:skipped_services][0]['reasons'] + end + progress_count += 1 + } + + vrp_with_solvers_file = File.new(Rails.root.join('test/fixtures/optimizer-wrapper/vrp-with-solvers-info.json')).read + vrp_complete_file = File.new(Rails.root.join('test/fixtures/optimizer-wrapper/vrp-completed.json')).read + + # Multiple responses for repeated requests + @stub_VrpSubmit = stub_request(:post, uri_template_post).to_return({ + status: 200, body: vrp_with_solvers_file, headers: {content_type: 'json'} + }) + stub_vrp_job = stub_request(:get, uri_template).to_return({ + status: 200, body: vrp_complete_file, headers: {content_type: 'json'} + }) + + @optim.optimize(@planning, @planning.routes, **{ optimize_time: 15000 }, &progress) + ensure + remove_request_stub(stub_vrp_job) if stub_vrp_job + end + + test 'should include solver information while working' do + uri_template_post = Addressable::Template.new('http://localhost:1791/0.1/vrp/submit.json') + uri_template = Addressable::Template.new('http://localhost:1791/0.1/vrp/jobs/{job_id}.json?api_key={api_key}') + + # Test with solver information but no progression + progress_count = 0 + progress = lambda{ |job_id, solution_data| + case progress_count + when 0 + assert_equal 'queued', solution_data[:status] + when 1 + assert_equal 'working', solution_data[:status] + when 2 + assert_equal 'completed', solution_data[:status] + end + + if solution_data && solution_data[:status] == 'queued' + assert_equal ['ortools'], solution_data[:solvers] + assert_equal 1, solution_data[:skipped_services].size + assert_equal 'vroom', solution_data[:skipped_services][0]['solver'] + assert_equal ['assert_no_relations_except_simple_shipments', 'assert_vehicles_no_duration_limit', 'assert_no_complex_setup_durations', 'assert_no_first_solution_strategy'], solution_data[:skipped_services][0]['reasons'] + assert_equal 0, solution_data[:first_progression] + assert_equal 0, solution_data[:second_progression] + end + + if solution_data && solution_data[:status] == 'working' + assert_equal ['ortools'], solution_data[:solvers] + assert_equal 1, solution_data[:skipped_services].size + assert_equal 'vroom', solution_data[:skipped_services][0]['solver'] + assert_equal ['assert_no_relations_except_simple_shipments', 'assert_vehicles_no_duration_limit', 'assert_no_complex_setup_durations', 'assert_no_first_solution_strategy'], solution_data[:skipped_services][0]['reasons'] + assert_equal 100, solution_data[:first_progression] + assert_equal 17.0, solution_data[:second_progression] + end + progress_count += 1 + } + + vrp_with_solvers_file = File.new(Rails.root.join('test/fixtures/optimizer-wrapper/vrp-with-solvers-info.json')).read + vrp_simple_progression_file = File.new(Rails.root.join('test/fixtures/optimizer-wrapper/vrp-simple-progression.json')).read + vrp_complete_file = File.new(Rails.root.join('test/fixtures/optimizer-wrapper/vrp-completed.json')).read + + # Multiple responses for repeated requests + @stub_VrpSubmit = stub_request(:post, uri_template_post).to_return({ + status: 200, body: vrp_with_solvers_file, headers: {content_type: 'json'} + }) + stub_vrp_job = stub_request(:get, uri_template).to_return({ + status: 200, body: vrp_simple_progression_file, headers: {content_type: 'json'} + }, { + status: 200, body: vrp_complete_file, headers: {content_type: 'json'} + }) + + @optim.optimize(@planning, @planning.routes, **{ optimize_time: 15000 }, &progress) + ensure + remove_request_stub(stub_vrp_job) if stub_vrp_job + end end From f72d84b809435e6c373ef86070ac10d1e2094c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 3 Jul 2025 15:36:03 +0200 Subject: [PATCH 04/24] Customer list - display job as json --- app/views/customers/_job.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/customers/_job.html.erb b/app/views/customers/_job.html.erb index ee8cb9056..62a16d0bc 100644 --- a/app/views/customers/_job.html.erb +++ b/app/views/customers/_job.html.erb @@ -11,8 +11,8 @@ <% end %> From f059bd40d8300a4b8a364341d646eb9302f6077d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Fri, 4 Jul 2025 09:56:26 +0200 Subject: [PATCH 05/24] solver priority order --- .../javascripts/active_inactive_drag_drop.js | 382 ++++++++++++++++++ app/assets/javascripts/application.js | 3 + app/assets/javascripts/customers.js | 6 + .../active_inactive_drag_drop.css.scss | 242 +++++++++++ app/controllers/customers_controller.rb | 3 +- app/jobs/optimizer_job.rb | 3 +- app/models/customer.rb | 2 +- app/views/customers/_form.html.erb | 41 ++ config/available_solvers.yml | 16 + config/initializers/available_solvers.rb | 16 + config/locales/en.yml | 7 + config/locales/fr.yml | 7 + lib/optim/optimizer_wrapper.rb | 1 + .../admin/customers_controller_test.rb | 16 + test/models/customer_test.rb | 24 ++ 15 files changed, 766 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/active_inactive_drag_drop.js create mode 100644 app/assets/stylesheets/active_inactive_drag_drop.css.scss create mode 100644 config/available_solvers.yml create mode 100644 config/initializers/available_solvers.rb diff --git a/app/assets/javascripts/active_inactive_drag_drop.js b/app/assets/javascripts/active_inactive_drag_drop.js new file mode 100644 index 000000000..628180919 --- /dev/null +++ b/app/assets/javascripts/active_inactive_drag_drop.js @@ -0,0 +1,382 @@ +// Active/Inactive Drag & Drop Component +// Usage: new ActiveInactiveDragDrop(containerId, options) +class ActiveInactiveDragDrop { + constructor(containerId, options = {}) { + this.containerId = containerId; + this.options = { + // Container selectors + activeContainerSelector: '.active-zone .item-list, .priority-active .priority-labels', + inactiveContainerSelector: '.inactive-zone .item-list, .priority-inactive .priority-labels', + + // Item selectors + itemSelector: '.draggable-item, .label', + + // Input selectors + hiddenInputSelector: 'input[name*="priority"]', + + // Display selectors + orderDisplaySelector: '.item-order, .order', + textDisplaySelector: '.item-text, .name', + + // Configuration + minActiveItems: 1, + orderFormat: 'number', // 'number', 'letter', 'custom' + inactiveOrderDisplay: '-', + + // Callbacks + onUpdate: null, + onValidationError: null, + + // CSS classes + activeZoneClass: 'active-zone', + inactiveZoneClass: 'inactive-zone', + itemListClass: 'item-list', + draggableItemClass: 'draggable-item', + orderDisplayClass: 'item-order', + textDisplayClass: 'item-text', + inactiveItemClass: 'inactive', + dragOverClass: 'drag-over', + + ...options + }; + + this.container = document.getElementById(containerId); + if (!this.container) { + console.error(`Container with id "${containerId}" not found`); + return; + } + + this.activeContainer = this.container.querySelector(this.options.activeContainerSelector); + this.inactiveContainer = this.container.querySelector(this.options.inactiveContainerSelector); + + if (!this.activeContainer || !this.inactiveContainer) { + console.error('Active or inactive container not found'); + return; + } + + this.draggedElement = null; + this.init(); + } + + init() { + this.makeDraggable(); + this.updateItemOrder(); + this.updateHiddenInputs(); + this.addFormValidation(); + this.addEventListeners(); + } + + addEventListeners() { + [this.activeContainer, this.inactiveContainer].forEach(container => { + container.addEventListener('dragstart', this.handleDragStart.bind(this)); + container.addEventListener('dragend', this.handleDragEnd.bind(this)); + container.addEventListener('dragover', this.handleDragOver.bind(this)); + container.addEventListener('dragleave', this.handleDragLeave.bind(this)); + container.addEventListener('drop', this.handleDrop.bind(this)); + }); + } + + updateItemOrder() { + // Update order for active items + const activeItems = this.activeContainer.querySelectorAll(this.options.itemSelector); + activeItems.forEach((item, index) => { + const orderElement = item.querySelector(this.options.orderDisplaySelector); + if (orderElement) { + orderElement.textContent = this.formatOrder(index + 1); + } + item.dataset.index = index; + + // Remove inactive class from active items + item.classList.remove(this.options.inactiveItemClass); + }); + + // Update order for inactive items + const inactiveItems = this.inactiveContainer.querySelectorAll(this.options.itemSelector); + inactiveItems.forEach((item, index) => { + const orderElement = item.querySelector(this.options.orderDisplaySelector); + if (orderElement) { + orderElement.textContent = this.options.inactiveOrderDisplay; + } + item.dataset.index = index; + + // Add inactive class to inactive items + item.classList.add(this.options.inactiveItemClass); + }); + } + + formatOrder(index) { + switch (this.options.orderFormat) { + case 'letter': + return String.fromCharCode(64 + index); // A, B, C, ... + case 'custom': + return this.options.orderFormat(index); + default: + return index.toString(); + } + } + + updateHiddenInputs() { + const activeItems = this.activeContainer.querySelectorAll(this.options.itemSelector); + const hiddenInputs = this.container.querySelectorAll(this.options.hiddenInputSelector); + + // Get the input name before removing existing inputs + const inputName = this.getHiddenInputName(); + + // Remove all existing hidden inputs + hiddenInputs.forEach(input => input.remove()); + + // Create new hidden inputs for active items + activeItems.forEach(item => { + const value = item.dataset.value || item.dataset.id || item.textContent.trim(); + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = inputName; + hiddenInput.value = value; + this.container.appendChild(hiddenInput); + }); + + // Call callback if provided + if (this.options.onUpdate) { + this.options.onUpdate(this.getActiveItems(), this.getInactiveItems()); + } + } + + getHiddenInputName() { + // Try to find existing input to get the name pattern + const existingInput = this.container.querySelector(this.options.hiddenInputSelector); + if (existingInput) { + return existingInput.name; + } + return 'priority[]'; + } + + getActiveItems() { + return Array.from(this.activeContainer.querySelectorAll(this.options.itemSelector)) + .map(item => ({ + id: item.dataset.id, + value: item.dataset.value, + text: item.textContent.trim(), + element: item + })); + } + + getInactiveItems() { + return Array.from(this.inactiveContainer.querySelectorAll(this.options.itemSelector)) + .map(item => ({ + id: item.dataset.id, + value: item.dataset.value, + text: item.textContent.trim(), + element: item + })); + } + + makeDraggable() { + const items = this.container.querySelectorAll(this.options.itemSelector); + items.forEach(item => { + item.draggable = true; + }); + } + + addFormValidation() { + const form = this.container.closest('form'); + if (form) { + form.addEventListener('submit', (e) => { + const activeItems = this.activeContainer.querySelectorAll(this.options.itemSelector); + if (activeItems.length < this.options.minActiveItems) { + e.preventDefault(); + const errorMessage = this.options.onValidationError ? + this.options.onValidationError() : + `At least ${this.options.minActiveItems} item(s) must be active`; + alert(errorMessage); + return false; + } + }); + } + } + + // Event handlers + handleDragStart(e) { + this.draggedElement = e.target; + e.target.style.opacity = '0.5'; + e.target.classList.add('dragging'); + } + + handleDragEnd(e) { + e.target.style.opacity = '1'; + e.target.classList.remove('dragging'); + this.draggedElement = null; + + // Clean up all drag-over indicators + this.container.querySelectorAll(`${this.options.itemSelector}, .${this.options.itemListClass}`).forEach(element => { + element.classList.remove(this.options.dragOverClass); + }); + } + + handleDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const target = e.target.closest(this.options.itemSelector); + const targetContainer = e.target.closest(`.${this.options.itemListClass}`); + + // Remove drag-over class from all containers + this.container.querySelectorAll(`.${this.options.itemListClass}`).forEach(container => { + container.classList.remove(this.options.dragOverClass); + }); + + if (target && target !== this.draggedElement) { + target.classList.add(this.options.dragOverClass); + } else if (targetContainer && targetContainer !== this.draggedElement.closest(`.${this.options.itemListClass}`)) { + // Highlight the container if dropping on empty space + targetContainer.classList.add(this.options.dragOverClass); + } + } + + handleDragLeave(e) { + const target = e.target.closest(this.options.itemSelector); + const targetContainer = e.target.closest(`.${this.options.itemListClass}`); + + if (target) { + target.classList.remove(this.options.dragOverClass); + } + + // Only remove container highlight if we're leaving the container entirely + if (targetContainer && !targetContainer.contains(e.relatedTarget)) { + targetContainer.classList.remove(this.options.dragOverClass); + } + } + + handleDrop(e) { + e.preventDefault(); + + const target = e.target.closest(this.options.itemSelector); + const targetContainer = e.target.closest(`.${this.options.itemListClass}`); + const draggedContainer = this.draggedElement.closest(`.${this.options.itemListClass}`); + + // Clean up all drag-over indicators + this.container.querySelectorAll(`${this.options.itemSelector}, .${this.options.itemListClass}`).forEach(element => { + element.classList.remove(this.options.dragOverClass); + }); + + if (this.draggedElement) { + // If dropping on an item + if (target && target !== this.draggedElement) { + // If moving between containers + if (targetContainer !== draggedContainer) { + // Check if we're trying to move to inactive zone and it would leave active zone empty + if (targetContainer === this.inactiveContainer && + draggedContainer === this.activeContainer && + this.activeContainer.children.length <= this.options.minActiveItems) { + // Don't allow moving the last active item to inactive + return; + } + // Move the element to the new container + targetContainer.appendChild(this.draggedElement); + } else { + // Reorder within the same container + const draggedIndex = parseInt(this.draggedElement.dataset.index); + const targetIndex = parseInt(target.dataset.index); + + if (draggedIndex < targetIndex) { + target.parentNode.insertBefore(this.draggedElement, target.nextSibling); + } else { + target.parentNode.insertBefore(this.draggedElement, target); + } + } + } + // If dropping on empty container + else if (targetContainer && targetContainer !== draggedContainer) { + // Check if we're trying to move to inactive zone and it would leave active zone empty + if (targetContainer === this.inactiveContainer && + draggedContainer === this.activeContainer && + this.activeContainer.children.length <= this.options.minActiveItems) { + // Don't allow moving the last active item to inactive + return; + } + // Move the element to the empty container + targetContainer.appendChild(this.draggedElement); + } + + // Update order numbers and hidden inputs + this.updateItemOrder(); + this.updateHiddenInputs(); + } + } + + // Public methods + refresh() { + this.makeDraggable(); + this.updateItemOrder(); + this.updateHiddenInputs(); + } + + addItem(itemData, active = true) { + const item = this.createItemElement(itemData); + const container = active ? this.activeContainer : this.inactiveContainer; + container.appendChild(item); + this.refresh(); + } + + createItemElement(itemData) { + const item = document.createElement('div'); + item.className = this.options.draggableItemClass; + item.draggable = true; + + if (itemData.id) item.dataset.id = itemData.id; + if (itemData.value) item.dataset.value = itemData.value; + + const orderSpan = document.createElement('span'); + orderSpan.className = this.options.orderDisplayClass; + orderSpan.textContent = this.options.inactiveOrderDisplay; + + const textSpan = document.createElement('span'); + textSpan.className = this.options.textDisplayClass; + textSpan.textContent = itemData.text || itemData.value || itemData.id; + + item.appendChild(orderSpan); + item.appendChild(textSpan); + + return item; + } + + removeItem(itemId) { + const item = this.container.querySelector(`[data-id="${itemId}"]`); + if (item) { + item.remove(); + this.refresh(); + } + } + + destroy() { + // Remove event listeners if needed + const form = this.container.closest('form'); + if (form) { + form.removeEventListener('submit', this.addFormValidation); + } + } +} + +// Helper function to initialize with common configurations +function initializeDragDrop(containerId, type = 'generic', options = {}) { + const defaultOptions = { + minActiveItems: 1, + onValidationError: function() { + return I18n.t(`${type}.form.priority_not_empty`); + } + }; + + return new ActiveInactiveDragDrop(containerId, { ...defaultOptions, ...options }); +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Auto-initialize common patterns + const containers = document.querySelectorAll('[data-drag-drop]'); + containers.forEach(container => { + const type = container.dataset.dragDrop; + const options = container.dataset.dragDropOptions ? + JSON.parse(container.dataset.dragDropOptions) : {}; + + initializeDragDrop(container.id, type, options); + }); +}); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 763e20cd6..c1fffbbf4 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -57,6 +57,9 @@ //= require mustache //= require_tree ../../templates +// Custom components +//= require active_inactive_drag_drop + // jQuery Turbolinks documentation informs to load all scripts before turbolinks //= require jquery.turbolinks //= require turbolinks diff --git a/app/assets/javascripts/customers.js b/app/assets/javascripts/customers.js index 1699da97e..b3383a61d 100644 --- a/app/assets/javascripts/customers.js +++ b/app/assets/javascripts/customers.js @@ -270,6 +270,12 @@ const customers_edit = function (params) { $("#customer_enable_optimization_soft_upper_bound").click(function() { $("#optimization_soft_upper_bound").toggleClass('d-none'); }); + + // Initialize solver priority drag & drop when DOM is ready + $(document).ready(function() { + // The solver priority drag & drop is now handled by the generic ActiveInactiveDragDrop component + // See app/assets/javascripts/active_inactive_drag_drop.js + }); }; var routersAllowedForProfile = function(params) { diff --git a/app/assets/stylesheets/active_inactive_drag_drop.css.scss b/app/assets/stylesheets/active_inactive_drag_drop.css.scss new file mode 100644 index 000000000..f49d6fcc7 --- /dev/null +++ b/app/assets/stylesheets/active_inactive_drag_drop.css.scss @@ -0,0 +1,242 @@ +// Active/Inactive Drag & Drop Component Styles + +@import "variables"; + +.active-inactive-zones { + display: flex; + gap: 20px; + margin: 10px 0; + align-items: stretch; + + .priority-labels { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin: 10px 0; + min-height: 50px; + border: 2px dashed transparent; + border-radius: 8px; + padding: 10px; + transition: all 0.2s ease; + } + + .priority-labels:empty { + border-color: $grey-color; + background: $lightgrey-color; + } + + .priority-labels.drag-over { + border-color: $success-color; + background: $success-background-color; + } + + .priority-zones { + display: flex; + gap: 30px; + flex-wrap: wrap; + margin-bottom: 10px; + align-items: stretch; + } + + .priority-active, .priority-inactive { + flex: 1 1 200px; + min-width: 200px; + display: flex; + flex-direction: column; + } + + .zone-title { + text-align: center; + font-size: 12px; + color: $text-black; + margin-top: 4px; + margin-bottom: 10px; + flex-shrink: 0; + } + + .inactive { + opacity: 0.5; + background: $lightgrey-color; + border-style: dashed; + cursor: move; + } + + .inactive .order { + background: $grey-color; + color: $white-background; + } + + .label input[type="hidden"] { + display: none; + } + + .label { + display: flex; + align-items: center; + background: $lightgrey-color; + border: 2px solid $grey-color; + border-radius: 8px; + padding: 8px 12px; + cursor: move; + user-select: none; + transition: all 0.2s ease; + position: relative; + min-width: 80px; + justify-content: space-between; + + &:hover { + border-color: $primary-color; + background: $primary-background-color; + } + + &.dragging { + opacity: 0.5; + transform: rotate(-5deg); + z-index: 1000; + } + + &.drag-over { + border-color: $success-color; + background: $success-background-color; + } + } +} + +.active-zone, +.inactive-zone { + flex: 1; + min-width: 200px; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.item-list { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin: 10px 0; + min-height: 50px; + border: 2px dashed transparent; + border-radius: 8px; + padding: 10px; + transition: all 0.2s ease; + background: #f8f9fa; + flex: 1; + align-content: flex-start; +} + +.item-list:empty { + border-color: #dee2e6; + background: #f8f9fa; +} + +.item-list.drag-over { + border-color: $success-color; + background: $success-background-color; +} + +.draggable-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #fff; + border: 1px solid #ddd; + border-radius: 6px; + cursor: grab; + transition: all 0.2s ease; + user-select: none; + min-width: 120px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.draggable-item:hover { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + transform: translateY(-1px); +} + +.draggable-item:active { + cursor: grabbing; +} + +.draggable-item.drag-over { + border-color: $success-color; + background: $success-background-color; + transform: scale(1.05); +} + +.draggable-item.inactive { + opacity: 0.6; + background: #f8f9fa; + border-color: #dee2e6; +} + +.item-order { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: $primary-color; + color: $white-background; + border-radius: 50%; + font-size: 12px; + font-weight: bold; + flex-shrink: 0; +} + +.draggable-item.inactive .item-order { + background: #6c757d; +} + +.item-text { + font-weight: 500; + color: #333; + flex: 1; + text-align: center; +} + +.draggable-item.inactive .item-text { + color: #6c757d; +} + +.zone-title { + font-weight: 600; + color: #495057; + margin-bottom: 8px; + text-align: center; + font-size: 14px; +} + +.active-zone .zone-title { + color: $success-color; +} + +.inactive-zone .zone-title { + color: $grey-color; +} + +// Responsive design +@media (max-width: 768px) { + .active-inactive-zones { + flex-direction: column; + gap: 15px; + } + + .active-zone, + .inactive-zone { + min-width: auto; + } + + .draggable-item { + min-width: 100px; + padding: 6px 10px; + } + + .item-order { + width: 20px; + height: 20px; + font-size: 11px; + } +} diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index 2223c48f7..7cdbd1cf7 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -249,7 +249,8 @@ def customer_params :strict_restriction, :low_emission_zone ], - devices: RecursiveParamsHelper.permit_recursive(devices_params) + devices: RecursiveParamsHelper.permit_recursive(devices_params), + advanced_options: { solver_priority: [] } ) return parameters else diff --git a/app/jobs/optimizer_job.rb b/app/jobs/optimizer_job.rb index 067aa31a9..1c5f37d49 100644 --- a/app/jobs/optimizer_job.rb +++ b/app/jobs/optimizer_job.rb @@ -114,7 +114,8 @@ def job_options(planning) optimize_minimal_time: planning.customer.optimization_minimal_time || @@optimize_minimal_time, enable_optimization_soft_upper_bound: planning.customer.enable_optimization_soft_upper_bound, vehicle_max_upper_bound: planning.customer.vehicle_max_upper_bound, - stop_max_upper_bound: planning.customer.stop_max_upper_bound + stop_max_upper_bound: planning.customer.stop_max_upper_bound, + solver_priority: planning.customer.solver_priority } end end diff --git a/app/models/customer.rb b/app/models/customer.rb index bbcc92f43..e974d4102 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -52,7 +52,7 @@ class Customer < ApplicationRecord include HashBoolAttr store_accessor :router_options, :time, :distance, :avoid_zones, :isochrone, :isodistance, :traffic, :track, :motorway, :toll, :low_emission_zone, :trailers, :weight, :weight_per_axle, :height, :width, :length, :hazardous_goods, :max_walk_distance, :approach, :snap, :strict_restriction hash_bool_attr :router_options, :time, :distance, :avoid_zones, :isochrone, :isodistance, :traffic, :track, :motorway, :toll, :low_emission_zone, :strict_restriction - store_accessor :advanced_options, :import + store_accessor :advanced_options, :import, :solver_priority include LocalizedAttr # To use to_delocalized_decimal method diff --git a/app/views/customers/_form.html.erb b/app/views/customers/_form.html.erb index 974a8491a..f3c1e8c53 100644 --- a/app/views/customers/_form.html.erb +++ b/app/views/customers/_form.html.erb @@ -295,6 +295,47 @@ <%= t '.optimization_parameters' %> +
+ +
+ <% available_solvers = Planner::Application.config.available_solvers %> + <% current_priority = (@customer.solver_priority || available_solvers).select { |s| available_solvers.include?(s) } %> + <% inactive_solvers = available_solvers - current_priority %> + +
+
+
+ <% current_priority.each_with_index do |solver, index| %> +
+ <%= solver.upcase %> + +
+ <% end %> +
+
<%= t('.solver_priority_active') %>
+
+
+
+ <% inactive_solvers.each_with_index do |solver, index| %> +
+ <%= solver.upcase %> +
+ <% end %> +
+
<%= t('.solver_priority_inactive') %>
+
+
+ <%= t '.solver_priority_help' %> +
+
<%= f.number_field :optimization_max_split_size, { help: t('.optimization_max_split_size_help'), placeholder: t('.optimization_max_split_size_default', n: LocalizedValues.localize_numeric_value(Planner::Application.config.optimize_max_split_size)), min: 0, append: content_tag('i', '', class: 'fa fa-gears fa-fw')} %> <%= f.number_field :optimization_cluster_size, { help: t('.optimization_cluster_size_help'), placeholder: t('.optimization_cluster_size_default', n: LocalizedValues.localize_numeric_value(Planner::Application.config.optimize_cluster_size)), min: 0, append: t('all.unit.second')} %> diff --git a/config/available_solvers.yml b/config/available_solvers.yml new file mode 100644 index 000000000..2d018f414 --- /dev/null +++ b/config/available_solvers.yml @@ -0,0 +1,16 @@ +production: + solvers: + - pyvrp + - vroom + - ortools + +development: + solvers: + - pyvrp + - vroom + - ortools + +test: + solvers: + - foo + - bar diff --git a/config/initializers/available_solvers.rb b/config/initializers/available_solvers.rb new file mode 100644 index 000000000..fc0dc7739 --- /dev/null +++ b/config/initializers/available_solvers.rb @@ -0,0 +1,16 @@ +require 'yaml' + +config_path = Rails.root.join('config', 'available_solvers.yml') +if File.exist?(config_path) + solvers_config = YAML.load_file(config_path) + env_config = solvers_config[Rails.env] || {} + solvers = env_config['solvers'] || [] +else + solvers = [] +end + +if ENV['AVAILABLE_SOLVERS'].present? + solvers = ENV['AVAILABLE_SOLVERS'].split(',').map(&:strip) +end + +Rails.application.config.available_solvers = solvers diff --git a/config/locales/en.yml b/config/locales/en.yml index aeabfae30..6d549b3e9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1074,6 +1074,13 @@ en: more the delay before time slot opening is important (0 to disallow the waiting time). optimization_force_start_help: Force vehicle start to the beginning of its schedule. + solver_priority: Optimization solver priority order + solver_priority_help: >- + Define the priority order of optimization solvers. If the problem is electible to the first solver, it will be used, + otherwise the second solver will be used, and so on. + solver_priority_active: Active solvers + solver_priority_inactive: Inactive solvers + solver_priority_not_empty: At least one solver must be active enable_optimization_soft_upper_bound_help: Enable overtimes in optimization enable_global_optimization_test: Enabled if Test enable_sms_intransit_help: Enable SMS in transit when the driver is on the way diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3d57c93b9..c99cfbb81 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1101,6 +1101,13 @@ fr: Valeurs conseillées :
- Au plus rapide : 0 à 1
- Au plus court : non pris en compte optimization_force_start_help: Forcer le départ du véhicule à son horaire de début. + solver_priority: Ordre de priorité des solveurs d'optimisation + solver_priority_help: >- + Définissez l'ordre de priorité des solveurs d'optimisation. Si le problème est éligible au premier solveur, il sera utilisé, + sinon le deuxième solveur sera utilisé, et ainsi de suite. + solver_priority_active: Solveurs actifs + solver_priority_inactive: Solveurs inactifs + solver_priority_not_empty: Au moins un solveur doit être actif enable_optimization_soft_upper_bound_help: Autoriser les dépassements horaires durant l'optimisation enable_global_optimization_test: Actif si Test enable_sms_intransit_help: Activer l'envoi de SMS d'approche lorsque le chauffeur est en route diff --git a/lib/optim/optimizer_wrapper.rb b/lib/optim/optimizer_wrapper.rb index 345ca0775..fef51851b 100644 --- a/lib/optim/optimizer_wrapper.rb +++ b/lib/optim/optimizer_wrapper.rb @@ -184,6 +184,7 @@ def build_configuration(**options) resolution: { duration: optim_duration_max, initial_time_out: optim_duration_min, + solver_priority: options[:solver_priority], strict_skills: options[:strict_skills], time_out_multiplier: 2 }, diff --git a/test/controllers/admin/customers_controller_test.rb b/test/controllers/admin/customers_controller_test.rb index 7d5df0285..2d9a01d5c 100644 --- a/test/controllers/admin/customers_controller_test.rb +++ b/test/controllers/admin/customers_controller_test.rb @@ -158,4 +158,20 @@ class Admin::CustomersControllerTest < ActionController::TestCase assert_redirected_to customers_path end + + test "should update customer with solver priority" do + patch :update, params: { + id: @customer, + customer: { + name: "Updated Customer", + advanced_options: { + solver_priority: ["pyvrp", "vroom"] + } + } + } + + assert_redirected_to edit_customer_path(assigns(:customer)) + @customer.reload + assert_equal ["pyvrp", "vroom"], @customer.solver_priority + end end diff --git a/test/models/customer_test.rb b/test/models/customer_test.rb index 574b8423c..c6909d256 100644 --- a/test/models/customer_test.rb +++ b/test/models/customer_test.rb @@ -491,4 +491,28 @@ def around assert @customer.update advanced_options: options assert_equal options, @customer.advanced_options end + + test 'should handle solver_priority in advanced_options' do + customer = customers(:customer_one) + + # Test setting solver priority + customer.solver_priority = ['pyvrp', 'vroom', 'ortools'] + customer.save! + + # Test reading solver priority + customer.reload + assert_equal ['pyvrp', 'vroom', 'ortools'], customer.solver_priority + + # Test empty solver priority + customer.solver_priority = [] + customer.save! + customer.reload + assert_equal [], customer.solver_priority + + # Test nil solver priority + customer.solver_priority = nil + customer.save! + customer.reload + assert_nil customer.solver_priority + end end From a926cbfbf84ee1c477d5e8a04ec6f260ee3d255a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Fri, 4 Jul 2025 14:41:37 +0200 Subject: [PATCH 06/24] Use new active-inactive d&g component on export modal --- app/assets/javascripts/plannings.js | 101 ++++++++++-------- .../active_inactive_drag_drop.css.scss | 23 ++++ app/views/layouts/_modal_csv.html.haml | 28 ++--- 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/app/assets/javascripts/plannings.js b/app/assets/javascripts/plannings.js index dc4f4b8df..a2fa35d11 100644 --- a/app/assets/javascripts/plannings.js +++ b/app/assets/javascripts/plannings.js @@ -106,9 +106,7 @@ const spreadsheetModalExport = function(columns, planningId, export_settings) { $(cb).prop('checked', export_settings['stops'].indexOf($(cb).val()) >= 0); }); } - $('.columns-export-list').sortable({ - connectWith: '#spreadsheet-columns .ui-sortable' - }); + var columnsExport = (export_settings && export_settings['export']) || []; var columnsSkip = (export_settings && export_settings['skips']) || []; if (columnsExport != []) { @@ -117,51 +115,59 @@ const spreadsheetModalExport = function(columns, planningId, export_settings) { columnsExport.push(c); }); } - var appendElement = function(parentSel, columnKey) { - var displayName; - var match = columnKey.match(new RegExp('^(.+)\\[(.*)\\]$')); - var rematch = columnKey.match(/^([a-z]+(?:_[a-z]+)*)(\d+)$/); - if (match) { - var export_translation = 'plannings.export_file.' + match[1]; - displayName = I18n.t(export_translation) + '[' + match[2] + ']'; - } - else if (rematch) { - var export_translation = 'plannings.export_file.' + rematch[1]; - displayName = I18n.t(export_translation) + rematch[2]; - } - else { - var export_translation = 'plannings.export_file.' + columnKey; - displayName = I18n.t(export_translation); - } - $(parentSel).append('
  • ' + displayName + '
  • '); - }; - $.each(columnsExport, function(i, c) { - if (columns.indexOf(c) >= 0) - appendElement('#columns-export', c); - }); - $.each(columnsSkip, function(i, c) { - if (columns.indexOf(c) >= 0) - appendElement('#columns-skip', c); - }); - $('#columns-export').find('a.remove').click(function(evt) { - var $elem = $(evt.currentTarget).closest('li'); - if ($elem.parent()[0].id === 'columns-export') { - var nextFocus = $elem.next(); - $('a.remove', $elem).hide(); - $('#columns-skip').append($elem); - if (nextFocus.length) $('a.remove', nextFocus).show(); + + // Nouvelle fonction pour injecter les colonnes dans la structure drag & drop + function renderSpreadsheetColumns(columnsExport, columnsSkip) { + const $export = $('#columns-export').empty(); + const $skip = $('#columns-skip').empty(); + + // Fonction pour obtenir le nom d'affichage traduit d'une colonne + function getDisplayName(columnKey) { + var displayName; + var match = columnKey.match(new RegExp('^(.+)\\[(.*)\\]$')); + var rematch = columnKey.match(/^([a-z]+(?:_[a-z]+)*)(\d+)$/); + if (match) { + var export_translation = 'plannings.export_file.' + match[1]; + displayName = I18n.t(export_translation) + '[' + match[2] + ']'; + } + else if (rematch) { + var export_translation = 'plannings.export_file.' + rematch[1]; + displayName = I18n.t(export_translation) + rematch[2]; + } + else { + var export_translation = 'plannings.export_file.' + columnKey; + displayName = I18n.t(export_translation); + } + return displayName; } - }); - $('#columns-export').find('li').mouseenter(function(evt) { - if ($(evt.currentTarget).closest('#columns-export').length > 0) - $('a.remove', evt.currentTarget).show(); - }).mouseleave(function(evt) { - $('a.remove', evt.currentTarget).hide(); - }); + + columnsExport.forEach(function(col) { + if (columns.indexOf(col) >= 0) { + $export.append( + `
    + + ${getDisplayName(col)} +
    ` + ); + } + }); + columnsSkip.forEach(function(col) { + if (columns.indexOf(col) >= 0) { + $skip.append( + `
    + + ${getDisplayName(col)} +
    ` + ); + } + }); + } + renderSpreadsheetColumns(columnsExport, columnsSkip); + if (export_settings && export_settings['format']) { $('[name=spreadsheet-format][value=' + export_settings['format'] + ']').prop('checked', true); } - $('#btn-spreadsheet').click(function() { + $('#btn-spreadsheet').off('click').on('click', function() { var planningsId = getPlanningsId(); if (!planningId && planningsId.length == 0) { warning(I18n.t('plannings.index.export.none_planning')); @@ -171,10 +177,11 @@ const spreadsheetModalExport = function(columns, planningId, export_settings) { var spreadsheetStops = $('.spreadsheet-stops:checked').map(function(i, e) { return $(e).val(); }).get().join('|'); - var spreadsheetColumnsExport = $('#columns-export').find('li').map(function(i, e) { + // Récupération via la nouvelle structure + var spreadsheetColumnsExport = $('#spreadsheet-columns-container .active-zone .item-list .draggable-item').map(function(i, e) { return $(e).attr('data-value'); }).get().join('|'); - var spreadsheetColumnsSkip = $('#columns-skip').find('li').map(function(i, e) { + var spreadsheetColumnsSkip = $('#spreadsheet-columns-container .inactive-zone .item-list .draggable-item').map(function(i, e) { return $(e).attr('data-value'); }).get().join('|'); var spreadsheetFormat = $('[name=spreadsheet-format]:checked').val(); @@ -184,7 +191,7 @@ const spreadsheetModalExport = function(columns, planningId, export_settings) { $('#planning-spreadsheet-modal').modal('toggle'); }); - $('.export_spreadsheet').click(function() { + $('.export_spreadsheet').off('click').on('click', function() { $('#planning-spreadsheet-modal').modal({ keyboard: true, show: true diff --git a/app/assets/stylesheets/active_inactive_drag_drop.css.scss b/app/assets/stylesheets/active_inactive_drag_drop.css.scss index f49d6fcc7..8413d270f 100644 --- a/app/assets/stylesheets/active_inactive_drag_drop.css.scss +++ b/app/assets/stylesheets/active_inactive_drag_drop.css.scss @@ -7,6 +7,7 @@ gap: 20px; margin: 10px 0; align-items: stretch; + max-height: 400px; .priority-labels { display: flex; @@ -109,6 +110,7 @@ display: flex; flex-direction: column; align-items: stretch; + max-height: 350px; } .item-list { @@ -124,6 +126,8 @@ background: #f8f9fa; flex: 1; align-content: flex-start; + overflow-y: auto; + max-height: 250px; } .item-list:empty { @@ -136,6 +140,24 @@ background: $success-background-color; } +.item-list::-webkit-scrollbar { + width: 6px; +} + +.item-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.item-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.item-list::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + .draggable-item { display: flex; align-items: center; @@ -149,6 +171,7 @@ user-select: none; min-width: 120px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + flex-shrink: 0; } .draggable-item:hover { diff --git a/app/views/layouts/_modal_csv.html.haml b/app/views/layouts/_modal_csv.html.haml index 88f5a23e2..eeaeabb8b 100644 --- a/app/views/layouts/_modal_csv.html.haml +++ b/app/views/layouts/_modal_csv.html.haml @@ -31,19 +31,21 @@ .row.row.form-group %label.col-md-3.control-label= t 'plannings.edit.dialog.spreadsheet.columns' .col-md-9 - #spreadsheet-columns.row - .col-md-6 - %label - %span - %i.fa.fa-check.fa-fw{style: "color: green;"} - = t 'plannings.edit.dialog.spreadsheet.columns_export' - %ol#columns-export.columns-export-list - .col-md-6 - %label - %span - %i.fa.fa-close.fa-fw{style: "color: red;"} - = t 'plannings.edit.dialog.spreadsheet.columns_skip' - %ul#columns-skip.columns-export-list + .active-inactive-zones#spreadsheet-columns-container{"data-drag-drop" => "spreadsheet", "data-drag-drop-options" => '{"minActiveItems":1,"activeContainerSelector":".active-zone .item-list","inactiveContainerSelector":".inactive-zone .item-list","itemSelector":".draggable-item","orderDisplaySelector":".item-order","textDisplaySelector":".item-text"}'} + .active-zone.col-md-6 + .zone-title + %label + %span + %i.fa.fa-check.fa-fw{style: "color: green;"} + = t 'plannings.edit.dialog.spreadsheet.columns_export' + .item-list#columns-export + .inactive-zone.col-md-6 + .zone-title + %label + %span + %i.fa.fa-close.fa-fw{style: "color: red;"} + = t 'plannings.edit.dialog.spreadsheet.columns_skip' + .item-list#columns-skip .row %p.help-block= t 'plannings.edit.dialog.spreadsheet.columns_DnD_help' .row.row.form-group.invisible From 202a0786bf9b325610f2553e93a7ebb401be056b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Mon, 7 Jul 2025 09:02:09 +0200 Subject: [PATCH 07/24] Put intermediate stops behind an option --- app/api/v01/customers.rb | 1 + app/api/v100/routes.rb | 28 +++++++------- app/controllers/customers_controller.rb | 1 + app/jobs/importer_destinations.rb | 3 ++ app/views/customers/_form.html.erb | 1 + app/views/routes/_in_route.html.haml | 38 ++++++++++--------- config/locales/en.yml | 2 + config/locales/fr.yml | 2 + ...924_add_enable_store_stops_to_customers.rb | 5 +++ db/structure.sql | 6 ++- test/api/v100/plannings_routes_test.rb | 18 +++++++++ test/jobs/importer_destinations_test.rb | 11 ++++++ 12 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 db/migrate/20250707061924_add_enable_store_stops_to_customers.rb diff --git a/app/api/v01/customers.rb b/app/api/v01/customers.rb index b9d2de6a1..9bc847f41 100644 --- a/app/api/v01/customers.rb +++ b/app/api/v01/customers.rb @@ -52,6 +52,7 @@ def customer_params :enable_global_optimization, :enable_vehicle_position, :enable_stop_status, + :enable_store_stops, :enable_sms, :enable_sms_intransit, :sms_template, diff --git a/app/api/v100/routes.rb b/app/api/v100/routes.rb index 2519448b2..447abfde2 100644 --- a/app/api/v100/routes.rb +++ b/app/api/v100/routes.rb @@ -13,8 +13,8 @@ class V100::Routes < Grape::API desc 'Move stop(s) to route. Append in order at end if automatic_insert is false.', detail: 'Set a new A route (or vehicle) for a stop which was in a previous B route in the same planning.', nickname: 'moveStops', - success: V01::Status.success(:code_204), - failure: V01::Status.failures + success: V100::Status.success(:code_204), + failure: V100::Status.failures params do requires :id, type: String, desc: SharedParams::ID_DESC requires :stop_ids, type: Array[Integer], desc: 'Ids separated by comma. You can specify ref (not containing comma) instead of id, in this case you have to add "ref:" before each ref, e.g. ref:ref1,ref:ref2,ref:ref3.', documentation: {param_type: 'form'}, coerce_with: CoerceArrayString @@ -33,25 +33,25 @@ class V100::Routes < Grape::API current_customer.save! end rescue VRPNoSolutionError - error! V01::Status.code_response(:code_304), 304 + error! V100::Status.code_response(:code_304), 304 end if planning.customer.job_optimizer - present planning.customer.job_optimizer, with: V01::Entities::Job + present planning.customer.job_optimizer, with: V100::Entities::Job else status 204 end end rescue Exceptions::JobInProgressError status 409 - present planning.customer.job_optimizer, with: V01::Entities::Job, message: I18n.t('errors.planning.already_optimizing') + present planning.customer.job_optimizer, with: V100::Entities::Job, message: I18n.t('errors.planning.already_optimizing') end end desc 'Move visit(s) to route. Append in order at end if automatic_insert is false.', detail: 'Set a new A route (or vehicle) for a visit which was in a previous B route in the same planning. Automatic_insert parameter allows to compute index of the stops created for visits.', nickname: 'moveVisits', - success: V01::Status.success(:code_204), - failure: V01::Status.failures + success: V100::Status.success(:code_204), + failure: V100::Status.failures params do requires :id, type: String, desc: SharedParams::ID_DESC requires :visit_ids, types: [Array[String], Array[Integer]], desc: 'Ids separated by comma. You can specify ref (not containing comma) instead of id, in this case you have to add "ref:" before each ref, e.g. ref:ref1,ref:ref2,ref:ref3.', documentation: {param_type: 'form'}, coerce_with: CoerceArrayString @@ -75,30 +75,32 @@ class V100::Routes < Grape::API current_customer.save! end rescue VRPNoSolutionError - error! V01::Status.code_response(:code_304), 304 + error! V100::Status.code_response(:code_304), 304 end if planning.customer.job_optimizer - present planning.customer.job_optimizer, with: V01::Entities::Job + present planning.customer.job_optimizer, with: V100::Entities::Job else status 204 end end rescue Exceptions::JobInProgressError status 409 - present planning.customer.job_optimizer, with: V01::Entities::Job, message: I18n.t('errors.planning.already_optimizing') + present planning.customer.job_optimizer, with: V100::Entities::Job, message: I18n.t('errors.planning.already_optimizing') end end desc 'Add intermediate store to route. Append in order at the end of the route', detail: 'Set a new StopStore to the route. index parameter allows to insert the stop at the provided index in the route.', nickname: 'addStopStore', - success: V01::Status.success(:code_204), - failure: V01::Status.failures + success: V100::Status.success(:code_204), + failure: V100::Status.failures params do requires :id, type: String, desc: SharedParams::ID_DESC requires :index, type: Integer end post ':route_id/stores/:id' do + error!(V100::Status.code_response(:code_401, after: I18n.t('errors.routes.enable_store_stops')), 401) if !current_customer.enable_store_stops + Route.includes_destinations.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:planning_id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) @@ -113,7 +115,7 @@ class V100::Routes < Grape::API end rescue Exceptions::JobInProgressError status 409 - present planning.customer.job_optimizer, with: V01::Entities::Job, message: I18n.t('errors.planning.already_optimizing') + present planning.customer.job_optimizer, with: V100::Entities::Job, message: I18n.t('errors.planning.already_optimizing') end end end diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index 7cdbd1cf7..703da5695 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -194,6 +194,7 @@ def customer_params :enable_global_optimization, :enable_vehicle_position, :enable_stop_status, + :enable_store_stops, :enable_sms, :enable_sms_intransit, :enable_optimization_soft_upper_bound, diff --git a/app/jobs/importer_destinations.rb b/app/jobs/importer_destinations.rb index a71272b9c..15624755c 100644 --- a/app/jobs/importer_destinations.rb +++ b/app/jobs/importer_destinations.rb @@ -423,6 +423,8 @@ def import_row(_name, row, line, _options) prepare_destination_in_planning(row, line, destination_attributes, visit_attributes) destination_attributes elsif is_store?(row[:stop_type]) + return nil unless @customer.enable_store_stops + store_attributes = build_store_attributes(row) prepare_store(row, line, store_attributes) prepare_store_in_planning(row, line, store_attributes) @@ -893,6 +895,7 @@ def prepare_visit_without_destination_ref(row, line, destination_index, destinat end def prepare_store_in_planning(row, line, store_attributes) + return if !@customer.enable_store_stops if store_attributes ref_planning = row[:planning_ref].blank? ? nil : row[:planning_ref].downcase if row.key?(:route) && store_attributes[:id].nil? diff --git a/app/views/customers/_form.html.erb b/app/views/customers/_form.html.erb index f3c1e8c53..09b51d057 100644 --- a/app/views/customers/_form.html.erb +++ b/app/views/customers/_form.html.erb @@ -289,6 +289,7 @@ <%= render partial: 'shared/check_box', locals: { form: f, field: :enable_global_optimization, label: t('activerecord.attributes.customer.enable_global_optimization'), help: (t('.enable_global_optimization_test') if !@customer.id || @customer.test) } %> <%= render partial: 'shared/check_box', locals: { form: f, field: :enable_vehicle_position, label: t('activerecord.attributes.customer.enable_vehicle_position') } %> <%= render partial: 'shared/check_box', locals: { form: f, field: :enable_stop_status, label: t('activerecord.attributes.customer.enable_stop_status') } %> + <%= render partial: 'shared/check_box', locals: { form: f, field: :enable_store_stops, label: t('activerecord.attributes.customer.enable_store_stops') } %> <%= render partial: 'shared/check_box', locals: { form: f, field: :enable_sms, label: t('activerecord.attributes.customer.enable_sms'), options: { disabled: @customer.reseller.messagings.none?{ |_k, v| v['enable'] == true } } } %> <%= f.number_field :history_cron_hour, { help: t('.history_cron_hour_help'), min: 0, max: 23, append: content_tag('i', '', class: 'fa fa-chart-line fa-fw')} %> diff --git a/app/views/routes/_in_route.html.haml b/app/views/routes/_in_route.html.haml index 090663288..46536be51 100644 --- a/app/views/routes/_in_route.html.haml +++ b/app/views/routes/_in_route.html.haml @@ -114,15 +114,16 @@ .btn-group %a.marker.btn.btn-default.btn-xs{href: "#", title: t("plannings.edit.marker_help")} %i.fa.fa-map-marker - .dropdown.btn-group - %button.btn.btn-default.btn-xs.dropdown-toggle{type: "button", "data-toggle": "dropdown", "aria-haspopup": "true", "aria-expanded": "false", title: t("plannings.edit.create_store_help")} - %i.fa.fa-plus - %ul.dropdown-menu.pull-right.store-dropdown{role: "menu"} - - @available_stores.each do |store| - %li - %a.dropdown-item{href: "#", "data-route-id": route[:route_id], "data-store-id": store[:id], "data-store-name": store[:name], class: "store-option"} - %i.fa.fa-fw{class: "#{store[:icon] || 'fa-store'}", style: "color: #{store[:color] || 'black'};"} - = store[:name] + - if current_user.customer.enable_store_stops + .dropdown.btn-group + %button.btn.btn-default.btn-xs.dropdown-toggle{type: "button", "data-toggle": "dropdown", "aria-haspopup": "true", "aria-expanded": "false", title: t("plannings.edit.create_store_help")} + %i.fa.fa-plus + %ul.dropdown-menu.pull-right.store-dropdown{role: "menu"} + - @available_stores.each do |store| + %li + %a.dropdown-item{href: "#", "data-route-id": route[:route_id], "data-store-id": store[:id], "data-store-name": store[:name], class: "store-option"} + %i.fa.fa-fw{class: "#{store[:icon] || 'fa-store'}", style: "color: #{store[:color] || 'black'};"} + = store[:name] = render partial: 'stops/list', locals: { route: route, summary: summary } %ul.stops.list-group{ style: ("display: none" if route[:hidden]) } %li.d-fake-flex.align-items-center.list-group-item{class: ('ui-state-error' if route[:store_stop]&.[](:error)), data: { store_id: route[:store_stop]&.[](:id)}.compact} @@ -165,12 +166,13 @@ .btn-group %a.marker.btn.btn-default.btn-xs{href: "#", title: t("plannings.edit.marker_help")} %i.fa.fa-map-marker - .dropdown.btn-group - %button.btn.btn-default.btn-xs.dropdown-toggle{type: "button", "data-toggle": "dropdown", "aria-haspopup": "true", "aria-expanded": "false", title: t("plannings.edit.create_store_help")} - %i.fa.fa-plus - %ul.dropdown-menu.pull-right.store-dropdown{role: "menu"} - - @available_stores.each do |store| - %li - %a.dropdown-item{href: "#", "data-route-id": route[:route_id], "data-store-id": store[:id], "data-store-name": store[:name], class: "store-option"} - %i.fa.fa-fw{class: "#{store[:icon] || 'fa-store'}", style: "color: #{store[:color] || 'black'};"} - = store[:name] + - if current_user.customer.enable_store_stops + .dropdown.btn-group + %button.btn.btn-default.btn-xs.dropdown-toggle{type: "button", "data-toggle": "dropdown", "aria-haspopup": "true", "aria-expanded": "false", title: t("plannings.edit.create_store_help")} + %i.fa.fa-plus + %ul.dropdown-menu.pull-right.store-dropdown{role: "menu"} + - @available_stores.each do |store| + %li + %a.dropdown-item{href: "#", "data-route-id": route[:route_id], "data-store-id": store[:id], "data-store-name": store[:name], class: "store-option"} + %i.fa.fa-fw{class: "#{store[:icon] || 'fa-store'}", style: "color: #{store[:color] || 'black'};"} + = store[:name] diff --git a/config/locales/en.yml b/config/locales/en.yml index 6d549b3e9..533f872e9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -190,6 +190,7 @@ en: enable_global_optimization: Enable global optimization enable_vehicle_position: Enable vehicle's position enable_stop_status: Enable visits status + enable_store_stops: Enable intermediate stores enable_sms: Enable SMS enable_sms_intransit: Enable SMS in transit enable_optimization_soft_upper_bound: Enable overtimes @@ -768,6 +769,7 @@ en: routes: bad_barcode_char: Barcode data content unsupported character expired: The tour has expired + enable_store_stops: Intermediate stops must be enabled import: csv: line: 'Line %{s}' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c99cfbb81..b627cd6be 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -190,6 +190,7 @@ fr: enable_global_optimization: Activer l'optimisation globale enable_vehicle_position: Activer la position des véhicules enable_stop_status: Activer les statuts des visites + enable_store_stops: Activer les sites intermédiaires enable_sms: Activer l'envoi de SMS enable_sms_intransit: Activer l'envoi de SMS d'approche enable_optimization_soft_upper_bound: Autoriser les dépassements horaires @@ -780,6 +781,7 @@ fr: routes: bad_barcode_char: Les données du code barre contiennent un caractère non pris en charge expired: La tournée a expiré + enable_store_stops: Les arrêts intermédiaires doivent être activés import: csv: line: 'Ligne %{s}' diff --git a/db/migrate/20250707061924_add_enable_store_stops_to_customers.rb b/db/migrate/20250707061924_add_enable_store_stops_to_customers.rb new file mode 100644 index 000000000..290027337 --- /dev/null +++ b/db/migrate/20250707061924_add_enable_store_stops_to_customers.rb @@ -0,0 +1,5 @@ +class AddEnableStoreStopsToCustomers < ActiveRecord::Migration[6.1] + def change + add_column :customers, :enable_store_stops, :boolean, default: false + end +end diff --git a/db/structure.sql b/db/structure.sql index d8aa1586d..714887343 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -158,7 +158,8 @@ CREATE TABLE public.customers ( visits_count integer DEFAULT 0 NOT NULL, plannings_count integer DEFAULT 0 NOT NULL, vehicles_count integer DEFAULT 0 NOT NULL, - enable_sms_intransit boolean DEFAULT false + enable_sms_intransit boolean DEFAULT false, + enable_store_stops boolean DEFAULT false ); @@ -3160,4 +3161,5 @@ INSERT INTO "schema_migrations" (version) VALUES ('20250618061806'), ('20250619092217'), ('20250626123719'), -('20250627085724'); +('20250627085724'), +('20250707061924'); diff --git a/test/api/v100/plannings_routes_test.rb b/test/api/v100/plannings_routes_test.rb index b4c861a80..54df03926 100644 --- a/test/api/v100/plannings_routes_test.rb +++ b/test/api/v100/plannings_routes_test.rb @@ -9,6 +9,7 @@ def app setup do @planning = plannings(:planning_one) + customers(:customer_one).update(enable_store_stops: true) end def around @@ -106,4 +107,21 @@ def api(planning_id, part = nil, param = {}) end end end + + test 'should not add store to route when enable_store_stops is false' do + customers(:customer_one).update(enable_store_stops: false) + route = @planning.routes.find{ |r| !r.vehicle_usage } + store = stores(:store_one) + + assert_no_difference('StopStore.count') do + post api(@planning.id, "/#{route.id}/stores/#{store.id}"), nil, input: { index: 0 }.to_json, CONTENT_TYPE: 'application/json' + assert_equal 401, last_response.status, last_response.body + + content = JSON.parse(last_response.body, symbolize_names: true) + assert_match I18n.t('errors.routes.enable_store_stops'), content[:message] + end + + route.reload + assert route.stops.none?{ |s| s.is_a?(StopStore) && s.store_id == @store.id } + end end diff --git a/test/jobs/importer_destinations_test.rb b/test/jobs/importer_destinations_test.rb index 28b2e7d8a..0647473eb 100644 --- a/test/jobs/importer_destinations_test.rb +++ b/test/jobs/importer_destinations_test.rb @@ -3,6 +3,7 @@ class ImporterDestinationsTest < ActionController::TestCase setup do @customer = customers(:customer_one) + @customer.update!(enable_store_stops: true) # Remove invalid stop stops(:stop_three_one).destroy @visit_tag1_count = @customer.visits.select{ |v| v.tags.include? tags(:tag_one) }.size @@ -754,4 +755,14 @@ def tempfile(file, name) end end end + + test 'should skip store stops when enable_store_stops is disabled' do + @customer.update!(enable_store_stops: false) + + assert_no_difference('StopVisit.count') do + assert_no_difference('StopStore.count') do + assert ImportCsv.new(importer: ImporterDestinations.new(@customer), replace: false, file: tempfile('test/fixtures/files/import_destinations_single_plan_two_routes_with_store.csv', 'text.csv')).import + end + end + end end From 85140b4530487bd0aecbbb62c08a3dd0bf2ba4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 17 Jul 2025 14:13:43 +0200 Subject: [PATCH 08/24] Fix store import tests --- test/api/v01/destinations_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/api/v01/destinations_test.rb b/test/api/v01/destinations_test.rb index 0a5c3c2f1..47ef1eb7c 100644 --- a/test/api/v01/destinations_test.rb +++ b/test/api/v01/destinations_test.rb @@ -227,6 +227,7 @@ def api(part = nil, param = {}) end test 'should create bulk from json with store destination' do + @customer.update(enable_store_stops: true) orig_locale = I18n.locale I18n.locale = :en @@ -1080,6 +1081,7 @@ def api(part = nil, param = {}) end test 'should import existing store with ref' do + @customer.update(enable_store_stops: true) orig_locale = I18n.locale I18n.locale = :en # Get existing store from fixtures From f8445953a3520610aa327a300a800b33dc100787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 11:22:39 +0200 Subject: [PATCH 09/24] Update Changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83550d5a..0ee221db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ ## Dev ### Added + - Customer: Allow to order and filter the solvers to use [#422](https://github.com/cartoway/planner-web/pull/422) + - Admin: Store the solver used and the ones skipped (with reasons) on the job info [#422](https://github.com/cartoway/planner-web/pull/422) ### Changed + - Export: Rework visually the columns selector [#422](https://github.com/cartoway/planner-web/pull/422) ### Removed From 937b30aa0628d247ecc922ba8ff39040684f9b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 10 Jul 2025 15:49:27 +0200 Subject: [PATCH 10/24] Introduce leaflet_lasso --- CHANGELOG.md | 1 + app/assets/javascripts/lasso.js | 430 ++++++++++++++++++ app/assets/javascripts/map_data_extractor.js | 94 ++++ app/assets/javascripts/plannings.js | 6 +- app/assets/javascripts/routes_layers.js | 32 ++ app/assets/stylesheets/plannings.css.scss | 92 ---- app/assets/stylesheets/scaffolds.css.scss | 92 ++++ app/controllers/plannings_controller.rb | 28 ++ app/helpers/shared_helper.rb | 36 ++ app/javascript/packs/vendor.js | 1 + app/javascript/packs/vendor_stylesheet.js | 2 + app/models/route.rb | 3 +- app/views/shared/_selection_details.html.haml | 58 +++ config/locales/en.yml | 10 + config/locales/fr.yml | 10 + config/routes.rb | 1 + package.json | 3 +- test/controllers/plannings_controller_test.rb | 118 +++++ test/helpers/shared_helper_test.rb | 92 ++++ yarn.lock | 19 + 20 files changed, 1033 insertions(+), 95 deletions(-) create mode 100644 app/assets/javascripts/lasso.js create mode 100644 app/assets/javascripts/map_data_extractor.js create mode 100644 app/helpers/shared_helper.rb create mode 100644 app/views/shared/_selection_details.html.haml create mode 100644 test/helpers/shared_helper_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee221db2..438766215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Dev ### Added - Customer: Allow to order and filter the solvers to use [#422](https://github.com/cartoway/planner-web/pull/422) + - Planning: Introduce a lasso to select multiple stops on the fly [#424](https://github.com/cartoway/planner-web/pull/424) - Admin: Store the solver used and the ones skipped (with reasons) on the job info [#422](https://github.com/cartoway/planner-web/pull/422) ### Changed diff --git a/app/assets/javascripts/lasso.js b/app/assets/javascripts/lasso.js new file mode 100644 index 000000000..e535d4a9c --- /dev/null +++ b/app/assets/javascripts/lasso.js @@ -0,0 +1,430 @@ +// Copyright © Cartoway, 2025 +// +// This file is part of Cartoway Planner. +// +// Cartoway Planner is free software. You can redistribute it and/or +// modify since you respect the terms of the GNU Affero General +// Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Cartoway Planner. If not, see: +// +// + +'use strict'; + +import { ajaxError, beforeSendWaiting, completeAjaxMap } from './ajax'; + +/****************** + * LassoModule + * + */ +export const LassoModule = (function() { + var lassoHandler = null; + var selectedLayers = []; + var isLassoActive = false; + var lassoControl = null; + var map = null; + var planningId = null; + var routesLayer = null; + var dataExtractor = null; + var waitingRoute = null; + var refreshRoute = null; + + const initLasso = function(mapInstance, planningIdParam, routesLayerInstance, routeWaitingFunc, refreshRouteFunc) { + if (lassoHandler) { + return; // Already initialized + } + + map = mapInstance; + planningId = planningIdParam; + routesLayer = routesLayerInstance; + + // Store the functions passed from plannings.js + waitingRoute = routeWaitingFunc; + refreshRoute = refreshRouteFunc; + + // Initialize data extractor if available + if (typeof MapDataExtractor !== 'undefined') { + dataExtractor = MapDataExtractor; + dataExtractor.initialize(map, routesLayer); + } + + // Create lasso handler with custom layer detection + lassoHandler = L.lasso(map, { + polygon: { + color: '#198754', + weight: 5, + fillColor: '#a3cfbb', + fillOpacity: 0.3, + dashArray: '20, 15' + }, + intersect: false + }); + + map.on('lasso.finished', function(event) { + if (typeof LassoSelection === 'function') { + LassoSelection(event, map, document.querySelector('.sidebar')); + } else { + onLassoFinished(event); + } + }); + addLassoControl(); + + return this; + } + + const addLassoControl = function() { + if (lassoControl) { + return; // Already added + } + + // Check if the existing selection system already has a lasso control + var existingLassoControl = document.querySelector('.leaflet-lasso'); + if (existingLassoControl) { + return; // Don't create a duplicate control + } + + lassoControl = L.Control.extend({ + options: { + position: 'topleft' + }, + + onAdd: function() { + var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-lasso'); + container.style.backgroundColor = 'white'; + container.style.width = '28px'; + container.style.height = '26px'; + + var button = L.DomUtil.create('a', '', container); + button.title = I18n.t('plannings.edit.lasso.toggle'); + + var icon = L.DomUtil.create('i', 'lasso-icon fa fa-mouse-pointer fa-lg', button); + icon.style.marginLeft = '2px'; + + container.onclick = function() { + toggleLasso(); + }; + + return container; + } + }); + + map.addControl(new lassoControl()); + } + + const toggleLasso = function() { + // Check if the existing selection system is active + var existingLassoControl = document.querySelector('.leaflet-lasso'); + if (existingLassoControl && existingLassoControl.classList.contains('active')) { + // Use the existing system's toggle + existingLassoControl.click(); + return; + } + + // Use our own toggle + if (isLassoActive) { + disableLasso(); + } else { + enableLasso(); + } + } + + const enableLasso = function() { + isLassoActive = true; + lassoHandler.enable(); + + // Update existing lasso control if it exists + var existingLassoControl = document.querySelector('.leaflet-lasso'); + if (existingLassoControl) { + existingLassoControl.querySelector('i').classList.add('fa-crosshairs'); + existingLassoControl.querySelector('i').classList.remove('fa-mouse-pointer'); + } else { + $('.lasso-icon').addClass('fa-crosshairs').removeClass('fa-mouse-pointer'); + } + + map.getContainer().style.cursor = 'crosshair'; + } + + const disableLasso = function() { + isLassoActive = false; + lassoHandler.disable(); + + // Update existing lasso control if it exists + var existingLassoControl = document.querySelector('.leaflet-lasso'); + if (existingLassoControl) { + existingLassoControl.querySelector('i').classList.remove('fa-crosshairs'); + existingLassoControl.querySelector('i').classList.add('fa-mouse-pointer'); + } else { + $('.lasso-icon').removeClass('fa-crosshairs').addClass('fa-mouse-pointer'); + } + + map.getContainer().style.cursor = ''; + + // Clear selection without calling clearLassoSelection to avoid recursion + if (selectedLayers) { + selectedLayers.forEach(function(layer) { + if (layer.getElement && layer.getElement()) { + var element = layer.getElement(); + if (element && element.classList) { + element.classList.remove('leaflet-lasso-selected'); + } + } + }); + selectedLayers = []; + } + } + + const onLassoFinished = function(event) { + selectedLayers = event.layers; + + if (selectedLayers.length > 0) { + // Highlight selected layers + selectedLayers.forEach(function(layer) { + if (layer.getElement) { + layer.getElement().classList.add('leaflet-lasso-selected'); + } + }); + + // Show selection info + showLassoInfoModal(); + } else { + // Reset lasso tool when selection is empty + disableLasso(); + } + } + + const showLassoInfoModal = function() { + // Remove existing modal + $('#lasso-info-modal').remove(); + + // Extract stop IDs from selected layers + var stopIds = []; + + if (selectedLayers && selectedLayers.length > 0) { + selectedLayers.forEach(function(layer) { + if (layer.properties && layer.properties.stop_id) { + stopIds.push(layer.properties.stop_id); + } + }); + } + + // Prepare AJAX parameters + var ajaxParams = { + planning_id: planningId, + stop_ids: stopIds.join(',') + }; + + // Load modal template via AJAX + $.ajax({ + url: '/plannings/' + planningId + '/selection_details.html', + type: 'GET', + data: ajaxParams, + dataType: 'html', + beforeSend: beforeSendWaiting, + success: function(modalHtml) { + $('body').append(modalHtml); + + var modal = $('#lasso-info-modal'); + setupModalEventHandlers(modal); + modal.modal('show'); + }, + error: ajaxError, + complete: completeAjaxMap + }); + } + + + + const setupModalEventHandlers = function(modal) { + modal.on('click', '#clear-lasso-selection', function(e) { + e.preventDefault(); + clearLassoSelection(); + }); + + modal.on('click', '#move-stops-btn', function(e) { + e.preventDefault(); + moveSelectedStopsToRoute(); + }); + + var routeSelect = modal.find('#route-select'); + if (routeSelect.length > 0) { + routeSelect.select2({ + placeholder: I18n.t('plannings.edit.lasso.select_route_placeholder'), + allowClear: true, + minimumResultsForSearch: -1, + templateResult: function(route) { + var routeColor = $(route.element).data('route-color'); + var routeName = route.text; + + if (routeColor) { + return $('
    ' + routeName + '
    '); + } + return routeName; + }, + templateSelection: function(route) { + var routeColor = $(route.element).data('route-color'); + var routeName = route.text; + + if (routeColor) { + return $('
    ' + routeName + '
    '); + } + return routeName; + } + }); + + routeSelect.on('change', function(e) { + var routeId = $(this).val(); + modal.find('#move-stops-btn').prop('disabled', !routeId); + }); + } + + modal.on('hidden.bs.modal', function() { + disableLasso(); + }); + } + + const moveSelectedStopsToRoute = function() { + if (!selectedLayers || selectedLayers.length === 0) { + return; + } + + var targetRouteId = $('#route-select').val(); + if (!targetRouteId) { + alert(I18n.t('plannings.edit.lasso.please_select_route')); + return; + } + + var stopIds = []; + var sourceRouteIds = []; + + // Use MapDataExtractor to get stop IDs if available + if (dataExtractor) { + var selectedIds = selectedLayers.map(function(layer) { + return layer.properties.stop_id; + }).filter(function(id) { return id; }); + + var allMarkers = dataExtractor.extractMarkersData(); + var selectedMarkers = allMarkers.filter(function(marker) { + return selectedIds.includes(marker.id) && marker.type === 'stop'; + }); + + stopIds = selectedMarkers.map(function(marker) { + return marker.id; + }); + + // Get source route IDs from selected markers + selectedMarkers.forEach(function(marker) { + if (marker.route_id) { + sourceRouteIds.push(marker.route_id); + } + }); + } else { + // Fallback to original method + var selectedStops = selectedLayers.filter(function(layer) { + return layer.properties && layer.properties.stop_id; + }); + + stopIds = selectedStops.map(function(layer) { + return layer.properties.stop_id; + }); + + // Get source route IDs from selected layers + selectedStops.forEach(function(layer) { + if (layer.properties && layer.properties.route_id) { + sourceRouteIds.push(layer.properties.route_id); + } + }); + } + + if (stopIds.length === 0) { + return; + } + + // Create array of unique route IDs (source routes + target route) + var allRouteIds = sourceRouteIds.concat([targetRouteId]); + var uniqueRouteIds = allRouteIds.filter(function(item, pos) { + return allRouteIds.indexOf(item) === pos; + }); + + // Send AJAX request to move stops + $.ajax({ + url: '/plannings/' + planningId + '/' + targetRouteId + '/move.json', + type: 'PATCH', + data: { + stop_ids: stopIds + }, + beforeSend: function() { + beforeSendWaiting(); + uniqueRouteIds.forEach(function(routeId) { + waitingRoute(routeId); + }); + }, + success: function(data, _status, xhr) { + if (xhr.status === 204) return; + + var routesToRefresh = data.route_ids || uniqueRouteIds; + + routesToRefresh.forEach(function(route_id) { + refreshRoute(planningId, route_id); + }); + + routesLayer.refreshRoutes(routesToRefresh, data.summary.routes); + clearLassoSelection(); + notice(I18n.t('plannings.edit.lasso.stops_moved_success')); + }, + error: ajaxError, + complete: completeAjaxMap + }); + } + + const clearLassoSelection = function() { + if (selectedLayers) { + selectedLayers.forEach(function(layer) { + if (layer.getElement && layer.getElement()) { + var element = layer.getElement(); + if (element && element.classList) { + element.classList.remove('leaflet-lasso-selected'); + } + } + }); + selectedLayers = []; + } + + // Close info modal + $('#lasso-info-modal').modal('hide'); + } + + const destroy = function() { + if (lassoHandler) { + lassoHandler.disable(); + lassoHandler = null; + } + + if (lassoControl) { + map.removeControl(lassoControl); + lassoControl = null; + } + + clearLassoSelection(); + + map = null; + planningId = null; + } + + // Return public API + return { + initLasso: initLasso, + waitingRoute: waitingRoute, + refreshRoute: refreshRoute, + clearLassoSelection: clearLassoSelection, + moveSelectedStopsToRoute: moveSelectedStopsToRoute, + destroy: destroy + }; +})(); + diff --git a/app/assets/javascripts/map_data_extractor.js b/app/assets/javascripts/map_data_extractor.js new file mode 100644 index 000000000..2593e6108 --- /dev/null +++ b/app/assets/javascripts/map_data_extractor.js @@ -0,0 +1,94 @@ +// Copyright © Cartoway, 2025 +// +// This file is part of Cartoway Planner. +// +// Cartoway Planner is free software. You can redistribute it and/or +// modify since you respect the terms of the GNU Affero General +// Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Cartoway Planner. If not, see: +// +// + +'use strict'; + +var _map, + _routesLayer; + +function initialize(map, routesLayer) { + _map = map; + _routesLayer = routesLayer; + return this; +} + +function extractMarkersData() { + var markers = []; + + if (_routesLayer && _routesLayer.clustersByRoute) { + Object.keys(_routesLayer.clustersByRoute).forEach(function(routeId) { + var cluster = _routesLayer.clustersByRoute[routeId]; + if (cluster && cluster.getLayers) { + var clusterMarkers = cluster.getLayers(); + clusterMarkers.forEach(function(layer) { + if (layer.getLatLng && layer.properties) { + markers.push({ + id: layer.properties.stop_id, + type: 'stop', + lat: layer.getLatLng().lat, + lng: layer.getLatLng().lng, + route_id: layer.properties.route_id, + stop_id: layer.properties.stop_id, + properties: layer.properties + }); + } + }); + } + }); + } + + if (_routesLayer && _routesLayer.markerStores) { + Object.keys(_routesLayer.markerStores).forEach(function(storeId) { + var marker = _routesLayer.markerStores[storeId]; + if (marker && marker.getLatLng && marker.properties) { + markers.push({ + id: marker.properties.store_id, + type: 'store', + lat: marker.getLatLng().lat, + lng: marker.getLatLng().lng, + route_id: marker.properties.route_id, + properties: marker.properties + }); + } + }); + } + + if (markers.length === 0 && _routesLayer && _routesLayer.getSelectableLayers) { + var layers = _routesLayer.getSelectableLayers(); + layers.forEach(function(layer) { + if (layer.getLatLng && layer.properties) { + markers.push({ + id: layer.properties.stop_id, + type: 'stop', + lat: layer.getLatLng().lat, + lng: layer.getLatLng().lng, + route_id: layer.properties.route_id, + stop_id: layer.properties.stop_id, + properties: layer.properties + }); + } + }); + } + + return markers; +} + +window.MapDataExtractor = { + initialize: initialize, + extractMarkersData: extractMarkersData +}; diff --git a/app/assets/javascripts/plannings.js b/app/assets/javascripts/plannings.js index a2fa35d11..c9677bdd9 100644 --- a/app/assets/javascripts/plannings.js +++ b/app/assets/javascripts/plannings.js @@ -675,7 +675,8 @@ export const plannings_edit = function(params) { colorsByRoute: params.colors_by_route, appBaseUrl: params.apiWeb ? '/api-web/0.1/' : '/', popupOptions: popupOptions, - disableClusters: params.disable_clusters + disableClusters: params.disable_clusters, + planningId: planning_id }).on('clickStop', function(stop) { enlightenStop({index: stop.index, routeId: stop.routeId}); }).addTo(map); @@ -2827,6 +2828,9 @@ export const plannings_edit = function(params) { // Init device global tools devicesObservePlanning.init($('#edit-planning')); + + // Initialize lasso functionality after all functions are defined + routesLayer.initLasso(panelLoading, refreshSidebarRoute); }; var plannings_show = function(params) { diff --git a/app/assets/javascripts/routes_layers.js b/app/assets/javascripts/routes_layers.js index b8a072f92..fcb921d13 100644 --- a/app/assets/javascripts/routes_layers.js +++ b/app/assets/javascripts/routes_layers.js @@ -29,6 +29,7 @@ import { completeAjaxMap, ajaxError } from '../../assets/javascripts/ajax'; +import { LassoModule } from './lasso'; /****************** * PopupModule @@ -946,5 +947,36 @@ export const RoutesLayer = L.FeatureGroup.extend({ this.layersByRoute = {}; this.clustersByRoute = {}; + }, + + // Lasso initialization + initLasso: function(panelLoadingFunc, refreshSidebarRouteFunc) { + if (typeof LassoModule !== 'undefined' && !document.querySelector('.leaflet-lasso')) { + LassoModule.initLasso(this.map, this.options.planningId, this, panelLoadingFunc, refreshSidebarRouteFunc); + + $(document).on('lasso:stopsMoved', function(event, data) { + if (data && data.routeIds && this.refreshRoutes) { + this.refreshRoutes(data.routeIds); + } + }.bind(this)); + } + + return this; + }, + + getSelectableLayers: function() { + var layers = []; + + Object.keys(this.clustersByRoute).forEach(function(routeId) { + this.clustersByRoute[routeId].getLayers().forEach(function(marker) { + layers.push(marker); + }); + }.bind(this)); + + Object.keys(this.markerStores).forEach(function(storeId) { + layers.push(this.markerStores[storeId]); + }.bind(this)); + + return layers; } }); diff --git a/app/assets/stylesheets/plannings.css.scss b/app/assets/stylesheets/plannings.css.scss index d514f9cea..b3f28a1bb 100644 --- a/app/assets/stylesheets/plannings.css.scss +++ b/app/assets/stylesheets/plannings.css.scss @@ -367,98 +367,6 @@ $sidebar-margins: 10px; } #planning { - .route-info { - white-space: nowrap; - margin: 2px; - padding: 7px 6px 2px 6px; - text-overflow: ellipsis; - overflow: hidden; - height: 34px; - - .fa { - margin-right: 4px; - } - - .fa.unit { - margin-left: 4px; - } - } - - .stop-info { - padding: 0 2px; - text-align: center; - height: 22px; - margin: 0 6px; - } - - .timewindow-info { - max-width: 99%; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .stop-label { - margin: 0 !important; - line-height: unset !important; - border-radius: 0 !important; - font-weight: 600 !important; - font: 14px/1.5 Helvetica Neue,Arial,Helvetica,sans-serif; - } - - .info.route-info, .info.stop-info { - border: 1px solid $grey-color; - background-color: $lightgrey-color; - } - - .primary.route-info, .primary.stop-info { - border: 1px solid $primary-border-color; - background-color: $primary-background-color; - } - - .success.route-info, .success.stop-info { - border: 1px solid $success-border-color; - background-color: $success-background-color; - } - - .danger.route-info, .danger.stop-info { - border: 1px solid $danger-border-color !important; - background-color: $warning-color !important; - background: #fef1ec image-url("ui-bg_glass_95_fef1ec_1x400") 50% 50% repeat-x; - color: $danger-color !important; - } - - .inactive.route-info { - border: 1px solid lightgray; - background-color: #eee; - color: #555 - } - - .info.stop-info { - border: 1px solid lightgray; - background-color: $primary-background-color; - } - - .stop-info { - .editable:hover { - cursor: pointer; - text-decoration-line: underline !important; - } - - input.editable { - border: solid darkgrey 1px; - background-color: unset; - border-radius: 4px; - font-size: unset; - height: 18px; - margin: 1px 0px; - padding: 0; - text-align: center; - width: 44px; - } - - } - .route-tools { display: flex; justify-content: space-between; diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss index 943b337d3..b443ea8f7 100644 --- a/app/assets/stylesheets/scaffolds.css.scss +++ b/app/assets/stylesheets/scaffolds.css.scss @@ -1026,3 +1026,95 @@ input.form-control.number-of-days { width: fit-content !important; min-width: 0; } + +.route-info { + white-space: nowrap; + margin: 2px; + padding: 7px 6px 2px 6px; + text-overflow: ellipsis; + overflow: hidden; + height: 34px; + + .fa { + margin-right: 4px; + } + + .fa.unit { + margin-left: 4px; + } +} + +.stop-info { + padding: 0 2px; + text-align: center; + height: 22px; + margin: 0 6px; +} + +.timewindow-info { + max-width: 99%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.stop-label { + margin: 0 !important; + line-height: unset !important; + border-radius: 0 !important; + font-weight: 600 !important; + font: 14px/1.5 Helvetica Neue,Arial,Helvetica,sans-serif; +} + +.info.route-info, .info.stop-info { + border: 1px solid $grey-color; + background-color: $lightgrey-color; +} + +.primary.route-info, .primary.stop-info { + border: 1px solid $primary-border-color; + background-color: $primary-background-color; +} + +.success.route-info, .success.stop-info { + border: 1px solid $success-border-color; + background-color: $success-background-color; +} + +.danger.route-info, .danger.stop-info { + border: 1px solid $danger-border-color !important; + background-color: $warning-color !important; + background: #fef1ec image-url("ui-bg_glass_95_fef1ec_1x400") 50% 50% repeat-x; + color: $danger-color !important; +} + +.inactive.route-info { + border: 1px solid lightgray; + background-color: #eee; + color: #555 +} + +.info.stop-info { + border: 1px solid lightgray; + background-color: $primary-background-color; +} + +.stop-info { + .editable:hover { + cursor: pointer; + text-decoration-line: underline !important; + } + + input.editable { + border: solid darkgrey 1px; + background-color: unset; + border-radius: 4px; + font-size: unset; + height: 18px; + margin: 1px 0px; + padding: 0; + text-align: center; + width: 44px; + } + +} diff --git a/app/controllers/plannings_controller.rb b/app/controllers/plannings_controller.rb index 5ca9dc95e..b1a4ef2eb 100644 --- a/app/controllers/plannings_controller.rb +++ b/app/controllers/plannings_controller.rb @@ -38,6 +38,7 @@ class PlanningsController < ApplicationController include Pagy::Backend include PlanningExport include PlanningsHelper + include SharedHelper def index @plannings = current_user.customer.plannings.select{ |planning| @@ -254,6 +255,33 @@ def modal end end + def selection_details + selected_stop_ids = params[:stop_ids]&.split(',') || [] + + @quantities = {} + @available_routes = [] + @selection_info = { stops_count: 0 } + + planning = current_user.customer.plannings.where(id: params[:id] || params[:planning_id]).preload_routes_without_stops.first! + @available_routes = planning_summary(planning)[:routes] + + if selected_stop_ids.any? + stops = Stop.joins(:route) + .where(routes: { planning_id: planning.id }) + .where(id: selected_stop_ids) + .includes_destinations + .only_stop_visits + + @selection_info[:stops_count] = stops.size + @quantities = aggregate_visit_quantities(planning.customer, stops.map(&:visit)) + end + + respond_to do |format| + format.html { render partial: 'shared/selection_details', layout: false } + format.json { render json: { quantities: @quantities, available_routes: @available_routes, selection_info: @selection_info } } + end + end + def switch respond_to do |format| begin diff --git a/app/helpers/shared_helper.rb b/app/helpers/shared_helper.rb new file mode 100644 index 000000000..562d0b523 --- /dev/null +++ b/app/helpers/shared_helper.rb @@ -0,0 +1,36 @@ +module SharedHelper + def aggregate_visit_quantities(customer, visits) + deliverable_units = customer.deliverable_units + + quantities = {} + deliverable_units.each do |unit| + quantities[unit.id] = { + id: unit.id, + label: unit.label, + icon: unit.default_icon, + has_pickup: false, + has_delivery: false, + pickup: 0, + delivery: 0 + } + end + + return quantities if visits.blank? + + visits.each do |visit| + next if !visit.is_a?(Visit) + + visit.default_pickups.each do |unit_id, quantity| + quantities[unit_id][:pickup] += quantity.to_f + quantities[unit_id][:has_pickup] ||= true if quantity.to_f > 0 + end + + visit.default_deliveries.each do |unit_id, quantity| + quantities[unit_id][:delivery] += quantity.to_f + quantities[unit_id][:has_delivery] ||= true if quantity.to_f > 0 + end + end + + quantities + end +end diff --git a/app/javascript/packs/vendor.js b/app/javascript/packs/vendor.js index eb1dfdd27..a0196e84c 100644 --- a/app/javascript/packs/vendor.js +++ b/app/javascript/packs/vendor.js @@ -60,6 +60,7 @@ import 'leaflet-hash'; import 'sidebar-v2/js/leaflet-sidebar'; import 'leaflet-responsive-popup'; import 'polyline-encoded'; +import 'leaflet-lasso'; import '../../assets/javascripts/screenLog.js.erb'; diff --git a/app/javascript/packs/vendor_stylesheet.js b/app/javascript/packs/vendor_stylesheet.js index e088dbbd0..3f090a557 100644 --- a/app/javascript/packs/vendor_stylesheet.js +++ b/app/javascript/packs/vendor_stylesheet.js @@ -38,6 +38,8 @@ import 'leaflet.markercluster/dist/MarkerCluster.css'; import 'leaflet.markercluster/dist/MarkerCluster.Default.css'; import 'sidebar-v2/css/leaflet-sidebar.css'; import 'leaflet-responsive-popup/leaflet.responsive.popup.css'; +import 'leaflet-lasso/src/lasso-handler.css'; +import 'leaflet-lasso/src/lasso-control.css'; import 'pnotify/dist/pnotify.css'; import 'pnotify/dist/pnotify.buttons.css'; import 'pnotify/dist/pnotify.history.css'; diff --git a/app/models/route.rb b/app/models/route.rb index 67dca7784..9f7deb24a 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -874,7 +874,8 @@ def stops_to_geojson_points(options = {}) number: vehicle_usage? ? stop.number(inactive_stops) : nil, color: stop.default_color, icon: stop.icon, - icon_size: stop.icon_size + icon_size: stop.icon_size, + stop_id: stop.id } } diff --git a/app/views/shared/_selection_details.html.haml b/app/views/shared/_selection_details.html.haml new file mode 100644 index 000000000..b5b0837b5 --- /dev/null +++ b/app/views/shared/_selection_details.html.haml @@ -0,0 +1,58 @@ +#lasso-info-modal.modal.fade{role: "dialog", tabindex: "-1"} + .modal-dialog + .modal-content + .modal-header + %button.close{"data-dismiss" => "modal", type: "button"} × + %h4.modal-title + %i.fa.fa-mouse-pointer.fa-fw + = t('plannings.edit.lasso.selection_info') + .modal-body + .container-fluid + .row + .col-md-12 + %h5 + %i.fa.fa-map-marker.fa-fw + = t('plannings.edit.lasso.selection_summary') + .row + .col-xs-4 + .primary.route-info + %i.fa.fa-check-square.fa-fw + = @selection_info[:stops_count] + = t('plannings.edit.stops') + - @quantities.each do |unit_id, quantity| + - [:pickup, :delivery].each do |sym| + - if quantity[sym] > 0 + .col-xs-4.route-data-advanced + .route-info.primary{ title: [quantity[:label], t('plannings.edit.route_quantity_help')].compact.join(' - ')} + %i.fa.fa-fw{class: quantity[:icon]} + - if sym == :pickup + %i.fa.fa-fw.fa-up-long{title: t("plannings.edit.pickup_help")} + = quantity[:pickup] + - if sym == :delivery + - if quantity[:has_pickup] + %i.fa.fa-fw.fa-down-long{title: t("plannings.edit.delivery_help")} + = quantity[:delivery] + - if quantity[:label] + = "#{quantity[:label]}" + + - if @selection_info[:stops_count] > 0 && @available_routes.any? + .col-md-12 + #lasso-route-selection + %h5 + %i.fa.fa-route.fa-fw + = t('plannings.edit.lasso.select_target_route') + .form-group + %select#route-select.form-control{style: "width: 100%;"} + %option{value: "", selected: true}= t('plannings.edit.lasso.select_route_placeholder') + - @available_routes.each do |route| + %option{value: route[:route_id], "data-route-name": route[:name], "data-route-color": route[:color] || ""} + = route[:name].blank? ? t('plannings.edit.out_of_route') : route[:name] + + .modal-footer + .container-fluid + .lasso-actions + %button#clear-lasso-selection.btn.btn-default{"data-dismiss" => "modal", type: "button"} + = t('all.verb.cancel') + %button#move-stops-btn.btn.btn-primary{disabled: true, type: "button"} + %i.fa.fa-arrow-right.fa-fw + = t('plannings.edit.lasso.move_stops') diff --git a/config/locales/en.yml b/config/locales/en.yml index 533f872e9..e4e9891ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1973,6 +1973,16 @@ en: send_sms_success: 'Succeed to send %{c} SMS' historize: Historize historize_success: Historize Succeed + lasso: + toggle: Enable/disable lasso selection + selection_info: Selection information + move_stops: Move stops + stops_moved_success: Stops moved successfully + stops_moved_error: Error moving stops + select_target_route: Transfer stops + select_route_placeholder: Choose a route... + please_select_route: Please select a destination route + selection_summary: Selection summary popup: outdated: Route trace outdated. Refresh your plan (Refresh button). time_window_start_end_1: 'Time window 1:' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b627cd6be..dbb533718 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2063,6 +2063,16 @@ fr: send_sms_success: 'Envoi de %{c} SMS avec succès' historize: Historiser historize_success: Historisé avec succès + lasso: + toggle: Activer/désactiver la sélection par lasso + selection_info: Informations de sélection + move_stops: Déplacer les arrêts + stops_moved_success: Arrêts déplacés avec succès + stops_moved_error: Erreur lors du déplacement des arrêts + select_target_route: Transférer les arrêts + select_route_placeholder: Choisissez une tournée... + please_select_route: Veuillez sélectionner une route de destination + selection_summary: Résumé de la sélection popup: outdated: >- Tracé de la tournée obsolète. Veuillez recalculer votre plan de diff --git a/config/routes.rb b/config/routes.rb index 96351da48..f71c35469 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -122,6 +122,7 @@ delete 'stores' => 'stores#destroy_multiple' resources :plannings do + get 'selection_details' => 'plannings#selection_details' patch ':route_id/:stop_id/move' => 'plannings#move' patch ':route_id/:stop_id/driver_move' => 'plannings#driver_move' patch ':route_id/:stop_id/move/:index' => 'plannings#move' diff --git a/package.json b/package.json index 434406327..514c6f695 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "dependencies": { - "@rails/webpacker": "3.2.1", "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/preset-react": "^7.23.2", + "@rails/webpacker": "3.2.1", "babel-loader": "^8.3.0", "bootstrap": "3.4.1", "bootstrap-datepicker": "1.7.1", @@ -19,6 +19,7 @@ "leaflet-control-geocoder": "1.5.8", "leaflet-draw": "0.4.9", "leaflet-hash": "0.2.1", + "leaflet-lasso": "^2.2.13", "leaflet-pip": "1.1.0", "leaflet-polylineoffset": "1.1.1", "leaflet-responsive-popup": "0.2.0", diff --git a/test/controllers/plannings_controller_test.rb b/test/controllers/plannings_controller_test.rb index 8762e8cb2..60eb87948 100644 --- a/test/controllers/plannings_controller_test.rb +++ b/test/controllers/plannings_controller_test.rb @@ -9,6 +9,7 @@ class PlanningsControllerTest < ActionController::TestCase @reseller = resellers(:reseller_one) request.host = @reseller.host @planning = plannings(:planning_one) + @stop = stops(:stop_one_one) @export_settings_params = { columns: 'ref_planning|planning|planning_date|route|vehicle|order|stop_type|active|wait_time|time|distance|drive_time|out_of_window|out_of_capacity|out_of_drive_time|out_of_force_position|out_of_work_time|out_of_max_distance|out_of_max_ride_distance|out_of_max_ride_duration|status|status_updated_at|eta|ref|name|street|detail|postalcode|city|country|lat|lng|comment|phone_number|tags|ref_visit|duration|time_window_start_1|time_window_end_1|time_window_start_2|time_window_end_2|priority|revenue|force_position|tags_visit|quantity1', skips: '', stops: 'out-of-route|store|rest|inactive'} @@ -915,4 +916,121 @@ def around end end end + + test "should get selection details modal with planning data" do + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: @stop.id.to_s + } + + assert_response :success + assert_template partial: 'shared/_selection_details' + + assert_equal 1, assigns(:selection_info)[:stops_count] + assert assigns(:quantities).any? + assert assigns(:available_routes).any? + end + + test "should get selection details modal with HTML format" do + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: @stop.id.to_s + } + + assert_response :success + assert_template partial: 'shared/_selection_details' + assert assigns(:quantities).any? + assert assigns(:available_routes).any? + end + + test "should handle multiple stop IDs" do + stop2 = stops(:stop_one_two) + stop2.visit.update(deliveries: { 1 => 30, 2 => 15 }) if stop2.visit + + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: "#{@stop.id},#{stop2.id}" + } + + assert_response :success + assert_equal 2, assigns(:selection_info)[:stops_count] + + quantities = assigns(:quantities) + assert quantities.any? + end + + test "should handle empty selection" do + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: '' + } + + assert_response :success + assert_equal 0, assigns(:selection_info)[:stops_count] + assert assigns(:quantities).empty? + end + + test "should handle invalid planning ID" do + get :selection_details, params: { + planning_id: 99999, + stop_ids: @stop.id.to_s + } + assert_response :not_found + end + + test "should include route information" do + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: @stop.id.to_s + } + + assert_response :success + routes = assigns(:available_routes) + assert routes.any? + + route = routes[1] + assert route[:route_id] + assert route[:name] + assert route[:vehicle_usage_id] + end + + test "should include out_of_route in available routes" do + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: @stop.id.to_s + } + + assert_response :success + routes = assigns(:available_routes) + + out_of_route_option = routes.find{ |r| r[:vehicle_usage_id].nil? } + assert_not_nil out_of_route_option, "Out of route should be included in available routes" + assert_equal '', out_of_route_option[:name] + assert_nil out_of_route_option[:color] + end + + test "should use helper methods for data processing" do + deliverable_unit = deliverable_units(:deliverable_unit_one_one) + @stop.visit.update(deliveries: { deliverable_unit.id => 50 }) if @stop.visit + + get :selection_details, params: { + planning_id: @planning.id, + stop_ids: @stop.id.to_s + } + + assert_response :success + + quantities = assigns(:quantities) + assert quantities.any? + + selection_info = assigns(:selection_info) + assert_equal 1, selection_info[:stops_count] + + routes = assigns(:available_routes) + assert routes.any? + route = routes[1] + assert_includes route.keys, :route_id + assert_includes route.keys, :name + assert_includes route.keys, :color + end end diff --git a/test/helpers/shared_helper_test.rb b/test/helpers/shared_helper_test.rb new file mode 100644 index 000000000..61d06cdfa --- /dev/null +++ b/test/helpers/shared_helper_test.rb @@ -0,0 +1,92 @@ +require 'test_helper' + +class SharedHelperTest < ActionView::TestCase + include SharedHelper + + setup do + @customer = customers(:customer_one) + @planning = plannings(:planning_one) + @deliverable_unit = deliverable_units(:deliverable_unit_one_one) + end + + test "aggregate_visit_quantities with empty visits" do + result = aggregate_visit_quantities(@customer, []) + + # Should return all deliverable units with zero quantities + assert result.key?(@deliverable_unit.id) + assert_equal 0, result[@deliverable_unit.id][:pickup] + assert_equal 0, result[@deliverable_unit.id][:delivery] + assert_equal false, result[@deliverable_unit.id][:has_pickup] + assert_equal false, result[@deliverable_unit.id][:has_delivery] + end + + test "aggregate_visit_quantities with nil visits" do + result = aggregate_visit_quantities(@customer, nil) + + # Should return all deliverable units with zero quantities + assert result.key?(@deliverable_unit.id) + assert_equal 0, result[@deliverable_unit.id][:pickup] + assert_equal 0, result[@deliverable_unit.id][:delivery] + assert_equal false, result[@deliverable_unit.id][:has_pickup] + assert_equal false, result[@deliverable_unit.id][:has_delivery] + end + + test "aggregate_visit_quantities with visits having deliveries" do + # Create mock visits with deliveries + visit1 = mock('visit1') + visit1.stubs(:is_a?).with(Visit).returns(true) + visit1.stubs(:default_pickups).returns({}) + visit1.stubs(:default_deliveries).returns({ @deliverable_unit.id => 10.5 }) + + visit2 = mock('visit2') + visit2.stubs(:is_a?).with(Visit).returns(true) + visit2.stubs(:default_pickups).returns({}) + visit2.stubs(:default_deliveries).returns({ @deliverable_unit.id => 5.5 }) + + result = aggregate_visit_quantities(@customer, [visit1, visit2]) + + # Check the specific deliverable unit we're testing + assert result.key?(@deliverable_unit.id) + assert_equal 0, result[@deliverable_unit.id][:pickup] + assert_equal 16.0, result[@deliverable_unit.id][:delivery] + assert_equal false, result[@deliverable_unit.id][:has_pickup] + assert_equal true, result[@deliverable_unit.id][:has_delivery] + end + + test "aggregate_visit_quantities with visits having pickups and deliveries" do + # Create mock visits with both pickups and deliveries + visit1 = mock('visit1') + visit1.stubs(:is_a?).with(Visit).returns(true) + visit1.stubs(:default_pickups).returns({ @deliverable_unit.id => 5.0 }) + visit1.stubs(:default_deliveries).returns({ @deliverable_unit.id => 10.0 }) + + visit2 = mock('visit2') + visit2.stubs(:is_a?).with(Visit).returns(true) + visit2.stubs(:default_pickups).returns({ @deliverable_unit.id => 3.0 }) + visit2.stubs(:default_deliveries).returns({ @deliverable_unit.id => 7.0 }) + + result = aggregate_visit_quantities(@customer, [visit1, visit2]) + + # Check the specific deliverable unit we're testing + assert result.key?(@deliverable_unit.id) + assert_equal 8.0, result[@deliverable_unit.id][:pickup] + assert_equal 17.0, result[@deliverable_unit.id][:delivery] + assert_equal true, result[@deliverable_unit.id][:has_pickup] + assert_equal true, result[@deliverable_unit.id][:has_delivery] + end + + test "aggregate_visit_quantities with non-visit objects" do + # Create mock non-visit objects + non_visit = mock('non_visit') + non_visit.stubs(:is_a?).with(Visit).returns(false) + + result = aggregate_visit_quantities(@customer, [non_visit]) + + # Should return all deliverable units with zero quantities since non-visit objects are skipped + assert result.key?(@deliverable_unit.id) + assert_equal 0, result[@deliverable_unit.id][:pickup] + assert_equal 0, result[@deliverable_unit.id][:delivery] + assert_equal false, result[@deliverable_unit.id][:has_pickup] + assert_equal false, result[@deliverable_unit.id][:has_delivery] + end +end diff --git a/yarn.lock b/yarn.lock index 4c60014dd..8af7f270b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -942,6 +942,18 @@ webpack "^3.10.0" webpack-manifest-plugin "^1.3.2" +"@terraformer/common@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@terraformer/common/-/common-2.1.2.tgz#7bf83f81f1c3a99069c714c044004f9707f00c97" + integrity sha512-cwPdTFzIpekZhZRrgDEkqLKNPoqbyCBQHiemaovnGIeUx0Pl336MY/eCxzJ5zXkrQLVo9zPalq/vYW5HnyKevQ== + +"@terraformer/spatial@^2.1.2": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@terraformer/spatial/-/spatial-2.2.1.tgz#81165d19570dc35454c620cf55e7bbc9ec44bf69" + integrity sha512-Zq2WbKJgZspurToBJ7KjmmHC7HbvNt/fH8Bh/kj+RgcGoM9OzurAXlEfIMrVQQIh3ypSbJn+hHQ33n/d805uBQ== + dependencies: + "@terraformer/common" "^2.1.2" + "@types/json-schema@^7.0.5": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -4838,6 +4850,13 @@ leaflet-hash@0.2.1: resolved "https://registry.yarnpkg.com/leaflet-hash/-/leaflet-hash-0.2.1.tgz#c36c718347c5243033b57cb4baea26119d82c701" integrity sha512-1aOP7j1hGDcOo5qAl9QjOcdu33FKQsQiCdepJfAVd7+qwBAZrObLzEGNL0BOUxjXWeJMn/Bv37FmWF2/oj6rLg== +leaflet-lasso@^2.2.13: + version "2.2.13" + resolved "https://registry.yarnpkg.com/leaflet-lasso/-/leaflet-lasso-2.2.13.tgz#015134715d6cb1fa4c7829e10718e13a77e927e1" + integrity sha512-XVGP2zoaEB4psLZttTK6K9i+wJckvxTD65um2vgDgTGutpEwxKpEBwCClnrSEK+zIMrJ9juhV5sd72XYKKgmlA== + dependencies: + "@terraformer/spatial" "^2.1.2" + leaflet-pip@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/leaflet-pip/-/leaflet-pip-1.1.0.tgz#fcf6777895531bc5bf883da9b663af0992424a01" From 628a698373fd2deabaaaf4dc28d998df3ccd228d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Tue, 17 Jun 2025 17:02:02 +0200 Subject: [PATCH 11/24] Refacto automatic insert --- CHANGELOG.md | 1 + app/api/v01/plannings.rb | 2 +- app/api/v100/destinations.rb | 2 +- app/controllers/plannings_controller.rb | 4 +- app/models/location.rb | 2 +- app/models/planning.rb | 243 ++++++++---------- app/models/store.rb | 4 + test/api/v01/plannings_test.rb | 9 +- test/api/v100/plannings_destinations_test.rb | 2 +- test/api/v100/plannings_test.rb | 8 +- test/controllers/plannings_controller_test.rb | 5 +- test/models/planning_test.rb | 2 +- 12 files changed, 135 insertions(+), 149 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 438766215..94e21d922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed - Export: Rework visually the columns selector [#422](https://github.com/cartoway/planner-web/pull/422) + - Rework of the automatic insert method [#423](https://github.com/cartoway/planner-web/pull/423) ### Removed diff --git a/app/api/v01/plannings.rb b/app/api/v01/plannings.rb index d284cc533..bd4380beb 100644 --- a/app/api/v01/plannings.rb +++ b/app/api/v01/plannings.rb @@ -187,7 +187,7 @@ def planning_params end patch ':id/automatic_insert' do Route.includes_destinations.scoping do - planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! + planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).preload_route_details.first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) stops = planning.routes.flat_map{ |r| r.stops }.select{ |stop| params[:stop_ids].include?(stop.id) } diff --git a/app/api/v100/destinations.rb b/app/api/v100/destinations.rb index 82fd118a9..154338245 100644 --- a/app/api/v100/destinations.rb +++ b/app/api/v100/destinations.rb @@ -29,7 +29,7 @@ class V100::Destinations < Grape::API optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end get ':id/candidate_insert' do - planning = current_customer.plannings.where(ParseIdsRefs.read(params[:planning_id])).first! + planning = current_customer.plannings.where(ParseIdsRefs.read(params[:planning_id])).preload_route_details.first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) destination = current_customer.destinations.where(ParseIdsRefs.read(params[:id])).first! diff --git a/app/controllers/plannings_controller.rb b/app/controllers/plannings_controller.rb index b1a4ef2eb..85908336d 100644 --- a/app/controllers/plannings_controller.rb +++ b/app/controllers/plannings_controller.rb @@ -326,8 +326,8 @@ def automatic_insert end end - if @planning.compute_saved - @routes = @planning.routes.select{ |r| route_ids.include? r.id } + if @planning.compute_saved && @planning.reload + @routes = @planning.routes.where(id: route_ids).includes_vehicle_usages.includes_destinations format.json { render action: :show } else format.json { render json: @planning.errors, status: :unprocessable_entity } diff --git a/app/models/location.rb b/app/models/location.rb index 761b85a7a..8030c5e36 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -113,7 +113,7 @@ def delay_geocode end def distance(position) - lat && lng && position.lat && position.lng && Math.hypot(position.lat - lat, position.lng - lng) + lat && lng && position.lat && position.lng && Math.hypot(position.lat - lat, position.lng - lng) || 2147483647 end def warnings diff --git a/app/models/planning.rb b/app/models/planning.rb index 5ca970f6b..f2ae389fe 100644 --- a/app/models/planning.rb +++ b/app/models/planning.rb @@ -288,8 +288,11 @@ def compute_saved!(options = {}) # Collect all segments from all routes that need routing all_segments = [] + computed_routes = [] + routes.each{ |r| if options[:bang]!= false || r.outdated && r.vehicle_usage? + computed_routes << r segments = r.collect_segments_for_routing(r.stops) all_segments << { route: r, segments: segments } if segments.any? end @@ -298,7 +301,7 @@ def compute_saved!(options = {}) # Batch process all segments in parallel precompute_traces(all_segments, options) - routes.each{ |r| + computed_routes.each{ |r| if options[:bang] == false r.compute(options.merge(skip_preload: true)) else @@ -310,12 +313,12 @@ def compute_saved!(options = {}) stop_stores += stops_by_type['StopStore'].to_a.map(&:import_attributes) } - Route.import(routes.map(&:import_attributes), validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) + Route.import(computed_routes.map(&:import_attributes), validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) StopVisit.import(stop_visits, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) StopRest.import(stop_rests, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) StopStore.import(stop_stores, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) - routes.each{ |r| + computed_routes.each{ |r| r.invalidate_route_cache && r.reload next unless Planner::Application.config.delayed_job_use @@ -467,7 +470,6 @@ def automatic_insert(stop, options = { exclusion: :locked }) # Take the closest routes visit and eval insert route, index = prefered_route_and_index(available_routes, stop, options) - if route stop.active = true move_stop(route, stop, index || 1) @@ -1002,146 +1004,125 @@ def quantities private - def prefered_route_and_index(available_routes, stop, options = {}) - stop_dup = stop.dup - options[:active_only] = true if options[:active_only].nil? - cache_sum_out_of_window = Hash.new{ |h, k| h[k] = k.sum_out_of_window } - tmp_routes = {} + def select_insertion_data(insertion_data) + insertion_by_route = insertion_data.group_by { |data| data[0] } + selected_insertions = [] - by_distance = available_routes.flat_map { |route| - route.compute # Update the eventual outdated route - index = 0 - stops = route.stops.map { |s| - next if s.id == stop.id || !stop.active && !options[:active_only] - - index += 1 - next if !(s.is_a?(StopVisit) && s.visit.destination.position?) - - [s.visit.destination, route, index] - }.compact - stops ||= [] - stops << [route.vehicle_usage.default_store_start, route, 1] if stops.empty? && route.vehicle_usage.default_store_start&.position? - stops << [route.vehicle_usage.default_store_stop, route, route.stops_size + 1] if route.vehicle_usage&.default_store_stop&.position? - stops - }.compact.sort_by{ |a| - a[0] && a[0].position? ? a[0].distance(stop_dup.position) : Float::INFINITY + # Select at least one insertion from each route + insertion_by_route.each{ |route, insertions| + best_insertion = insertions.min_by { |data| data[4] } # Sort by distance (data[2]) + selected_insertions << insertion_data.delete(best_insertion) } - return available_routes.first if by_distance.empty? - - # If more than one available_routes take at least one stop from second route - pos_second_route = by_distance.index{ |s| s[1].id != by_distance[0][1].id } if available_routes.size > 1 - # Take 5% from nearest stops (min: 3, max: 10) and a stop in second route if it exists - (by_distance[0..[9, [2, by_distance.size / 20].max].min] + - (pos_second_route ? [by_distance[pos_second_route]] : [])).flat_map{ |dest_route_idx| - [[dest_route_idx[1], dest_route_idx[2]], [dest_route_idx[1], dest_route_idx[2] + 1]] - }.uniq.map { |ri| - ri[0].class.amoeba do - clone :stops # Only duplicate stops just for compute evaluation - nullify :planning_id - end - tmp_routes[ri[0].id] = ri[0].amoeba_dup if !tmp_routes[ri[0].id] - r = tmp_routes[ri[0].id] - if stop_dup.is_a?(StopVisit) - if stop_dup.route_id == r.id - r.move_stop_out(stop_dup) - end - r.add(stop_dup.visit, ri[1], true) - else - r.add_or_update_rest(true) - end - r.compute(no_geojson: true, no_quantities: true) - - # Difference of total time + difference of sum of out_of_window time - ri[2] = ((r.end - r.start) - (ri[0].end && ri[0].start ? ri[0].end - ri[0].start : 0)) + (r.sum_out_of_window - cache_sum_out_of_window[ri[0]]) - # Delta distance - ri[3] = r.distance - ri[0].distance.to_f - - r.remove_visit(stop_dup.visit) if stop_dup.is_a?(StopVisit) - - # Return ri with time and distance added - ri - }.select { |ri| - # Check for max time or distance if any - route_available = true - route_available = ri[2].abs < options[:max_time] if options[:max_time] && route_available - route_available = ri[3].abs < options[:max_distance] if options[:max_distance] && route_available - route_available - }.min_by { |ri| - # Return route with the minimum time - ri[2] - } + # Add the 20 first insertion_data + selected_insertions += insertion_data.sort_by{ |data| data[4] }.first(20) if insertion_data.any? + + selected_insertions end - def prefered_route_from_destination(available_routes, destination, options = {}) + def collect_insertion_data(route, stop, options = {}) options[:active_only] = true if options[:active_only].nil? - cache_sum_out_of_window = Hash.new{ |h, k| h[k] = k.sum_out_of_window } - tmp_routes = {} + previous_position = route.vehicle_usage.default_store_start&.position || stop.position + insertion_data = [] + route.stops.map{ |s| + next if s.id == stop.id || !s.active && !options[:active_only] || !s.position? + + insertion_data << + [ + route, + s.index - (stop.route == route && s.index > stop.index ? 1 : 0), + segment = [ + [previous_position.lat, previous_position.lng, stop.position.lat, stop.position.lng], + [stop.position.lat, stop.position.lng, s.position.lat, s.position.lng], + [previous_position.lat, previous_position.lng, s.position.lat, s.position.lng] + ], + nil, + previous_position.distance(stop.position) + stop.position.distance(s.position) - previous_position.distance(s.position) + ] + previous_position = s.position + + } + next_position = route.vehicle_usage.default_store_stop&.position || stop.position + insertion_data << + [ + route, + route.stops.size + (stop.route == route ? 0 : 1), + segment = [ + [previous_position.lat, previous_position.lng, stop.position.lat, stop.position.lng], + [stop.position.lat, stop.position.lng, next_position.position.lat, next_position.position.lng], + [previous_position.lat, previous_position.lng, next_position.position.lat, next_position.position.lng] + ], + nil, + previous_position.distance(stop.position) + stop.position.distance(next_position) - previous_position.distance(next_position) + ] + + insertion_data + end - by_distance = available_routes.flat_map { |route| + def compute_detours(route, insertion_data) + segments = insertion_data.flat_map{ |a| + a[2] + } + vehicle = route.vehicle_usage.vehicle + + router_options = vehicle.default_router_options.symbolize_keys + router_options[:geometry] = false + router_options[:speed_multiplier] = vehicle.default_speed_multiplier + + traces = + vehicle.default_router.trace_batch(segments, vehicle.default_router_dimension, router_options) + + # update the segments with the detour distance provided by the router + insertion_data.each_index{ |index| + insertion_data[index][2] = traces[3 * index][0] + traces[3 * index + 1][0] - traces[3 * index + 2][0] + insertion_data[index][3] = traces[3 * index][1] + traces[3 * index + 1][1] - traces[3 * index + 2][1] + } + + insertion_data + end + + def prefered_route_and_index(available_routes, stop, options = {}) + min_detour = available_routes.flat_map { |route| route.compute # Update the eventual outdated route - stops = route.stops.select { |stop| - stop.is_a?(StopVisit) && - stop.visit.destination.position? && - (!options[:active_only] || stop.active) - } - stops = stops.map { |s| [s.visit.destination, route, s.index] } - stops ||= [] - stops << [route.vehicle_usage.default_store_start, route, 1] if stops.empty? && route.vehicle_usage.default_store_start&.position? - stops << [route.vehicle_usage.default_store_stop, route, route.stops.size + 1] if route.vehicle_usage&.default_store_stop&.position? - stops - }.compact.sort_by{ |a| - a[0] && a[0].position? ? a[0].distance(destination) : Float::INFINITY + insertion_data = collect_insertion_data(route, stop, options) + insertion_data = select_insertion_data(insertion_data) + compute_detours(route, insertion_data) + }.compact.select{ |a| + insertion_available = true + insertion_available = a[2] < options[:max_distance] if insertion_available && options[:max_distance] + insertion_available = a[3] < options[:max_time] if insertion_available && options[:max_time] + insertion_available + }.min_by{ |a| + a[3] #route time } - return available_routes.first if by_distance.empty? + return unless min_detour - tmp_visit = Visit.new(destination_id: destination.id) - # If more than one available_routes take at least one stop from second route - pos_second_route = by_distance.index{ |s| s[1].id != by_distance[0][1].id } if available_routes.size > 1 - # Take 5% from nearest stops (min: 3, max: 10) and a stop in second route if it exists - (by_distance[0..[9, [2, by_distance.size / 20].max].min] + - (pos_second_route ? [by_distance[pos_second_route]] : [])).flat_map{ |dest_route_idx| - [[dest_route_idx[1], dest_route_idx[2]], [dest_route_idx[1], dest_route_idx[2] + 1]] - }.uniq.map { |ri| - ri[0] = ri[0].preload_compute_scopes - ri[0].class.amoeba do - clone :stops # Only duplicate stops just for compute evaluation - nullify :planning_id - end + [min_detour[0], min_detour[1]] + end + + def prefered_route_from_destination(available_routes, destination, options = {}) + options[:active_only] = true if options[:active_only].nil? - tmp_routes[ri[0].id] = ri[0].amoeba_dup if !tmp_routes[ri[0].id] - r = tmp_routes[ri[0].id] - # Rebranch old references - r.vehicle_usage = ri[0].vehicle_usage - r.stops.each.with_index do |stop, index| - original_stop = ri[0].stops[index] - next unless stop.is_a?(StopVisit) + # Create a temporary visit for the destination + tmp_visit = Visit.new(destination_id: destination.id) + tmp_stop = StopVisit.new(visit: tmp_visit) - stop.visit = original_stop.visit - stop.visit.destination = original_stop.visit.destination - end - r.add(tmp_visit, ri[1], true) - r.compute(no_geojson: true, no_quantities: true) - - # Difference of total time + difference of sum of out_of_window time - ri[2] = ((r.end - r.start) - (ri[0].end && ri[0].start ? ri[0].end - ri[0].start : 0)) + (r.sum_out_of_window - cache_sum_out_of_window[ri[0]]) - # Delta distance - ri[3] = r.distance - ri[0].distance.to_f - - r.remove_visit(tmp_visit) - - # Return ri with time and distance added - ri - }.select { |ri| - # Check for max time or distance if any - route_available = true - route_available = ri[2].abs < options[:max_time] if options[:max_time] && route_available - route_available = ri[3].abs < options[:max_distance] if options[:max_distance] && route_available - route_available - }.min_by { |ri| - # Return route with the minimum time - ri[2] + # Collect insertion data for all routes + min_detour = available_routes.flat_map { |route| + route.compute # Update the eventual outdated route + insertion_data = collect_insertion_data(route, tmp_stop, options) + compute_detours(route, insertion_data) + }.compact.select{ |a| + insertion_available = true + insertion_available = a[2] < options[:max_distance] if insertion_available && options[:max_distance] + insertion_available = a[3] < options[:max_time] if insertion_available && options[:max_time] + insertion_available + }.min_by{ |a| + a[2] } + return unless min_detour + + min_detour end def prefered_route_data(available_routes, destination, options = {}) diff --git a/app/models/store.rb b/app/models/store.rb index 094b3d767..3452649fc 100644 --- a/app/models/store.rb +++ b/app/models/store.rb @@ -74,6 +74,10 @@ def default_icon_size icon_size || Planner::Application.config.store_icon_size_default end + def position + self + end + def outdated Route.transaction do routes_usage_set = vehicle_usage_set_starts.collect{ |vehicle_usage_set_start| diff --git a/test/api/v01/plannings_test.rb b/test/api/v01/plannings_test.rb index 979a2db1c..a2871c7f5 100644 --- a/test/api/v01/plannings_test.rb +++ b/test/api/v01/plannings_test.rb @@ -290,11 +290,12 @@ def around customers(:customer_one).update(job_optimizer_id: nil) unassigned_stop = @planning.routes.detect{ |route| !route.vehicle_usage }.stops.select(&:position?).first - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 50_000 }.to_json, CONTENT_TYPE: 'application/json' + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 60 }.to_json, CONTENT_TYPE: 'application/json' assert_equal 400, last_response.status assert_not @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id)} - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 100_000 }.to_json, CONTENT_TYPE: 'application/json' + # Regarding the stub the insertion detour is 60 + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 61 }.to_json, CONTENT_TYPE: 'application/json' assert_equal 204, last_response.status assert @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id) } end @@ -309,8 +310,8 @@ def around assert_equal 400, last_response.status assert_not @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id)} - # Regarding the stub the insertion is 1000 back and 1000 forth - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_distance: 2_001}.to_json, CONTENT_TYPE: 'application/json' + # Regarding the stub the insertion detour is 1000 + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_distance: 1_001}.to_json, CONTENT_TYPE: 'application/json' assert_equal 204, last_response.status assert @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id) } end diff --git a/test/api/v100/plannings_destinations_test.rb b/test/api/v100/plannings_destinations_test.rb index fd29c8a8e..b32aa8fcd 100644 --- a/test/api/v100/plannings_destinations_test.rb +++ b/test/api/v100/plannings_destinations_test.rb @@ -34,7 +34,7 @@ def api(planning_id, part = nil, param = {}) get api(@planning.id, "#{@destination.id}/candidate_insert") assert_equal 201, last_response.status, last_response.body data = JSON.parse(last_response.body) - assert_kind_of Float, data['distance'] + assert_kind_of Integer, data['distance'] assert_kind_of Integer, data['time'] assert_kind_of Integer, data['index'] assert_kind_of Hash, data['route'] diff --git a/test/api/v100/plannings_test.rb b/test/api/v100/plannings_test.rb index dc3f35e2b..5cce0f2b4 100644 --- a/test/api/v100/plannings_test.rb +++ b/test/api/v100/plannings_test.rb @@ -106,11 +106,11 @@ def around customers(:customer_one).update(job_optimizer_id: nil) unassigned_stop = @planning.routes.detect{ |route| !route.vehicle_usage }.stops.select(&:position?).first - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 50_000 }.to_json, CONTENT_TYPE: 'application/json' + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 60 }.to_json, CONTENT_TYPE: 'application/json' assert_equal 400, last_response.status assert_not @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id)} - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 100_000 }.to_json, CONTENT_TYPE: 'application/json' + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_time: 61 }.to_json, CONTENT_TYPE: 'application/json' assert_equal 201, last_response.status assert @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id) } end @@ -121,11 +121,11 @@ def around # Fixtures route time and distance are 1.5 (and should be updated) @planning.routes.each{ |r| r.outdated = true } - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_distance: 500}.to_json, CONTENT_TYPE: 'application/json' + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_distance: 1_000}.to_json, CONTENT_TYPE: 'application/json' assert_equal 400, last_response.status assert_not @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id)} - patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_distance: 2_001}.to_json, CONTENT_TYPE: 'application/json' + patch api("#{@planning.id}/automatic_insert"), nil, input: { stop_ids: [unassigned_stop.id], max_distance: 1_001}.to_json, CONTENT_TYPE: 'application/json' assert_equal 201, last_response.status assert @planning.routes.reload.select(&:vehicle_usage).any?{ |route| route.stop_ids.include?(unassigned_stop.id) } end diff --git a/test/controllers/plannings_controller_test.rb b/test/controllers/plannings_controller_test.rb index 60eb87948..be84f9832 100644 --- a/test/controllers/plannings_controller_test.rb +++ b/test/controllers/plannings_controller_test.rb @@ -640,15 +640,14 @@ def around test 'should automatic insert with skills' do skill = Tag.first - route_with_skill = @planning.routes.select(&:vehicle_usage).last - route_with_skill.vehicle_usage.update!(tags: [skill]) + route_with_skill = @planning.routes.find{ |route| route.vehicle_usage && route.vehicle_usage.vehicle.tags.include?(skill) } unaffected_stop = stops(:stop_unaffected) unaffected_stop.visit.update!(tags: [skill]) assert_nil unaffected_stop.route.vehicle_usage? patch :automatic_insert, params: { id: @planning.id, format: :json, stop_ids: [unaffected_stop.id] } assert_response :success - assert_equal unaffected_stop.reload.route, route_with_skill + assert_equal unaffected_stop.reload.route.id, route_with_skill.id end test 'should not automatic insert without correct skills' do diff --git a/test/models/planning_test.rb b/test/models/planning_test.rb index 485bf38ca..19c59522f 100644 --- a/test/models/planning_test.rb +++ b/test/models/planning_test.rb @@ -374,7 +374,7 @@ def optimizer_global(planning, routes, options) assert_kind_of Route, data[:route] assert_kind_of Integer, data[:index] assert_kind_of Integer, data[:time] - assert_kind_of Float, data[:distance] + assert_kind_of Integer, data[:distance] end end end From fc3577968bd5968b2b1004e4fd3551b9b79281b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 16 Jul 2025 11:15:55 +0200 Subject: [PATCH 12/24] Bump turbolinks to 5.2 --- CHANGELOG.md | 1 + Gemfile | 2 +- Gemfile.lock | 7 +- app/assets/javascripts/application.js | 19 ++--- app/assets/javascripts/custom_attributes.js | 5 +- app/assets/javascripts/plannings.js | 7 ++ app/assets/javascripts/scaffolds.js | 2 +- app/assets/javascripts/zonings.js | 4 +- app/controllers/concerns/link_back.rb | 19 +++-- test/controllers/concerns/link_back_test.rb | 83 +++++++++++++++++++++ 10 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 test/controllers/concerns/link_back_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e21d922..01c2a275c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Changed - Export: Rework visually the columns selector [#422](https://github.com/cartoway/planner-web/pull/422) - Rework of the automatic insert method [#423](https://github.com/cartoway/planner-web/pull/423) + - Bump turbolinks [#431](https://github.com/cartoway/planner-web/pull/431) ### Removed diff --git a/Gemfile b/Gemfile index 7fa59e369..b8cd00047 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gem 'uglifier', '< 4.0' # TODO: fixme with use strict functions should be declar gem 'coffee-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks -gem 'turbolinks', '< 5' # FIXME: turbolinks not working with anchors in url +gem 'turbolinks', '~> 5.2' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder' # bundle exec rake doc:rails generates the API under doc/api. diff --git a/Gemfile.lock b/Gemfile.lock index 54c68648d..3c152b1dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -779,8 +779,9 @@ GEM date timeliness (0.4.5) timeout (0.4.1) - turbolinks (2.5.4) - coffee-rails + turbolinks (5.2.1) + turbolinks-source (~> 5.2) + turbolinks-source (5.2.0) twitter-bootstrap-rails (3.2.2) actionpack (>= 3.1) execjs (>= 2.2.2, >= 2.2) @@ -975,7 +976,7 @@ DEPENDENCIES sys-cpu sys-filesystem tidy-html5! - turbolinks (< 5) + turbolinks (~> 5.2) twitter-bootstrap-rails uglifier (< 4.0) validates_timeliness diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c1fffbbf4..432208139 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -66,24 +66,17 @@ 'use strict'; -Turbolinks.enableProgressBar(); -// bug in Firefox 40 when printing multi pages with progress bar -window.onbeforeprint = function() { - Turbolinks.enableProgressBar(false); -}; -window.onafterprint = function() { - Turbolinks.enableProgressBar(); -}; - -$(document).ready(function() { +Turbolinks.setProgressBarDelay(100); + +$(document).on('turbolinks:load', function() { var startSpinner = function() { $('body').addClass('turbolinks_waiting'); }; var stopSpinner = function() { $('body').removeClass('turbolinks_waiting'); }; - $(document).on("page:fetch", startSpinner); - $(document).on("page:receive", stopSpinner); + document.addEventListener("turbolinks:request-start", startSpinner); + document.addEventListener("turbolinks:request-end", stopSpinner); var menuLeft = $('.menu-left'); var mainContent = $('.main'); @@ -99,7 +92,7 @@ $(document).ready(function() { mainContent.on("click", () => { menuLeft.removeClass("open") $('.menu-content.in').removeClass('in'); - }) + }); Paloma.start(); }); diff --git a/app/assets/javascripts/custom_attributes.js b/app/assets/javascripts/custom_attributes.js index 972cf05d9..60118e34c 100644 --- a/app/assets/javascripts/custom_attributes.js +++ b/app/assets/javascripts/custom_attributes.js @@ -2,10 +2,9 @@ import '../../assets/javascripts/scaffolds' -const custom_attributes_form = function() { - //for turbolinks, when clicking on link_to +$(document).on('turbolinks:load', function() { $('.selectpicker').selectpicker(); -}; +}); // Expose custom_attributes_array window.CustomAttributes = { diff --git a/app/assets/javascripts/plannings.js b/app/assets/javascripts/plannings.js index c9677bdd9..e4ced6330 100644 --- a/app/assets/javascripts/plannings.js +++ b/app/assets/javascripts/plannings.js @@ -666,6 +666,9 @@ export const plannings_edit = function(params) { params.geocoder = true; var map = mapInitialize(params); + setTimeout(function() { + map.invalidateSize(); + }, 200); var popupOptions = params.manage_planning; var routesLayer = new RoutesLayer(planning_id, { url_click2call: url_click2call, @@ -2924,3 +2927,7 @@ Paloma.controller('Plannings', { plannings_show(this.params); } }); + +$(document).on('turbolinks:load', function() { + $('.select2').select2({ theme: 'bootstrap' }); +}); diff --git a/app/assets/javascripts/scaffolds.js b/app/assets/javascripts/scaffolds.js index 9d0ec335a..816e07f83 100644 --- a/app/assets/javascripts/scaffolds.js +++ b/app/assets/javascripts/scaffolds.js @@ -17,7 +17,7 @@ // 'use strict'; -$(document).on('ready page:load', function() { +$(document).on('turbolinks:load', function() { $('[data-toggle="selection"]').toggleSelect(); $('input[data-change="filter"]').filterTable(); $('[type="checkbox"][data-toggle="disable-multiple-actions"]').toggleMultipleActions(); diff --git a/app/assets/javascripts/zonings.js b/app/assets/javascripts/zonings.js index 7d41a9a71..4aa8cb597 100644 --- a/app/assets/javascripts/zonings.js +++ b/app/assets/javascripts/zonings.js @@ -153,7 +153,9 @@ export const zonings_edit = function(params) { }); } - $(document).on('page:before-change', checkZoningChanges); + $(document).on('turbolinks:load', function() { + $(document).on('page:before-change', checkZoningChanges); + }); map.on(L.Draw.Event.DRAWSTART, function() { creating_drawing = true; diff --git a/app/controllers/concerns/link_back.rb b/app/controllers/concerns/link_back.rb index 8dc24aa0b..468c65aca 100644 --- a/app/controllers/concerns/link_back.rb +++ b/app/controllers/concerns/link_back.rb @@ -10,18 +10,21 @@ module LinkBack private def save_link_back - # session[:previous_url] is a Rails built-in variable to save last url. + return unless request.get? && request.format.html? + return unless request.headers['Turbolinks-Referrer'] + + # URI fragments #123 are not part of the referer URI + # TODO: It might be interesting to link back to it if request.format == Mime[:html] - referer_uri = request.referer ? URI.parse(request.referer) : nil + referer_uri = URI.parse(request.headers['Turbolinks-Referrer']) referer_params = referer_uri && referer_uri.query ? CGI.parse(referer_uri.query) : nil - referer_fragment = referer_uri && referer_uri.fragment if referer_uri && params['back'] - # FIXME: The controller is fired twice and cannot link_back to its referer - unless session[:link_back]&.include?("#") - session[:link_back] = referer_uri.path - session[:link_back] += '#' + referer_fragment if referer_fragment - end + session[:link_back] = referer_uri.path + elsif referer_uri && referer_params && referer_params['back'] + # Clear link_back if we're coming from a page with back=true to prevent infinite loops + session.delete(:link_back) elsif !(referer_uri && referer_params && referer_params['back']) + # Clear link_back if we're not coming from a page with back=true session.delete(:link_back) end end diff --git a/test/controllers/concerns/link_back_test.rb b/test/controllers/concerns/link_back_test.rb new file mode 100644 index 000000000..38ff2efe1 --- /dev/null +++ b/test/controllers/concerns/link_back_test.rb @@ -0,0 +1,83 @@ +require 'test_helper' + +class LinkBackTest < ActionController::TestCase + setup do + @reseller = resellers(:reseller_one) + request.host = @reseller.host + @destination = destinations(:destination_one) + sign_in users(:user_one) + @controller = DestinationsController.new + + @previous_url = 'http://test.host/previous' + request.headers['Turbolinks-Referrer'] = @previous_url + end + + test 'should save link_back when back parameter is present' do + get :edit, params: { id: @destination, back: true } + + assert_equal URI.parse(@previous_url).path, session[:link_back] + end + + test 'should not save link_back when back parameter is not present' do + get :edit, params: { id: @destination } + + assert_nil session[:link_back] + end + + test 'should redirect to link_back after successful update' do + get :edit, params: { id: @destination, back: true } + + assert_equal URI.parse(@previous_url).path, session[:link_back] + + patch :update, params: { + id: @destination, + destination: { + name: 'Updated Destination', + city: 'Updated City' + } + } + + assert_redirected_to URI.parse(@previous_url).path + assert_nil session[:link_back] + end + + test 'should redirect to default path when no link_back in session' do + request.headers['Turbolinks-Referrer'] = nil + get :edit, params: { id: @destination, back: true } + + patch :update, params: { + id: @destination, + destination: { + name: 'Updated Destination', + city: 'Updated City' + } + } + + assert_redirected_to edit_destination_path(@destination) + end + + test 'should handle referer with query parameters' do + request.headers['Turbolinks-Referrer'] = 'http://test.host/plannings/1/edit?with_stops=true' + + get :edit, params: { id: @destination, back: true } + + assert_equal '/plannings/1/edit', session[:link_back] + end + + test 'should handle referer with fragment but not redirect to it' do + # TODO: It might be interesting to link back to it + # It should be handled client side, as not sending it is an html specification + request.headers['Turbolinks-Referrer'] = 'http://test.host/plannings/1/edit#collapseVisit123' + + get :edit, params: { id: @destination, back: true } + + assert_equal '/plannings/1/edit', session[:link_back] + end + + test 'should handle missing referer gracefully' do + request.headers['Turbolinks-Referrer'] = nil + get :edit, params: { id: @destination, back: true } + + assert_nil session[:link_back] + end +end From 902dbab602fb4c764f598ef21f4515af4ede139f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 16 Jul 2025 14:47:31 +0200 Subject: [PATCH 13/24] Performance improvements --- CHANGELOG.md | 15 ++- app/api/v01/helper/planning_icalendar.rb | 4 +- app/api/v01/plannings.rb | 22 ++-- app/api/v01/routes.rb | 10 +- app/api/v01/stops.rb | 2 +- app/api/v100/plannings.rb | 4 +- app/api/v100/routes.rb | 16 +-- .../api_web/v01/plannings_controller.rb | 2 +- .../api_web/v01/routes_controller.rb | 6 +- .../api_web/v01/zonings_controller.rb | 6 +- app/controllers/order_arrays_controller.rb | 2 +- app/controllers/plannings_controller.rb | 69 +++++++----- app/controllers/routes_controller.rb | 2 +- app/controllers/zones_controller.rb | 6 +- app/controllers/zonings_controller.rb | 6 +- app/helpers/plannings_helper.rb | 4 +- app/jobs/importer_destinations.rb | 2 +- app/models/planning.rb | 105 ++++++++++-------- app/models/route.rb | 26 ++++- app/models/stop.rb | 16 ++- app/models/visit.rb | 2 +- app/views/plannings/_edit.html.haml | 10 +- app/views/plannings/show.json.jbuilder | 4 +- app/views/routes/_index.csv.ruby | 2 +- app/views/routes/_index.excel.ruby | 2 +- 25 files changed, 208 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c2a275c..9091806d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,17 @@ ## Dev ### Added - - Customer: Allow to order and filter the solvers to use [#422](https://github.com/cartoway/planner-web/pull/422) - - Planning: Introduce a lasso to select multiple stops on the fly [#424](https://github.com/cartoway/planner-web/pull/424) - - Admin: Store the solver used and the ones skipped (with reasons) on the job info [#422](https://github.com/cartoway/planner-web/pull/422) + - Customer: Allow to order and filter the solvers to use [#422](https://github.com/cartoway/planner-web/pull/422) + - Planning: Introduce a lasso to select multiple stops on the fly [#424](https://github.com/cartoway/planner-web/pull/424) + - Admin: Store the solver used and the ones skipped (with reasons) on the job info [#422](https://github.com/cartoway/planner-web/pull/422) ### Changed - - Export: Rework visually the columns selector [#422](https://github.com/cartoway/planner-web/pull/422) - - Rework of the automatic insert method [#423](https://github.com/cartoway/planner-web/pull/423) - - Bump turbolinks [#431](https://github.com/cartoway/planner-web/pull/431) + - Export: Rework visually the columns selector [#422](https://github.com/cartoway/planner-web/pull/422) + - Rework of the automatic insert method [#423](https://github.com/cartoway/planner-web/pull/423) + - Bump turbolinks [#431](https://github.com/cartoway/planner-web/pull/431) + - Routes: + - Add stores to scopes [#435](https://github.com/cartoway/planner-web/pull/435) + - Ensure compute_saved! is in a transaction [#435](https://github.com/cartoway/planner-web/pull/435) ### Removed diff --git a/app/api/v01/helper/planning_icalendar.rb b/app/api/v01/helper/planning_icalendar.rb index 2d37b38cc..cb9026d92 100644 --- a/app/api/v01/helper/planning_icalendar.rb +++ b/app/api/v01/helper/planning_icalendar.rb @@ -41,7 +41,7 @@ def add_route_to_calendar(calendar, route) def planning_calendar(planning) calendar = Icalendar::Calendar.new create_timezone calendar - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning.routes.select(&:vehicle_usage_id).each do |route| add_route_to_calendar calendar, route end @@ -51,7 +51,7 @@ def planning_calendar(planning) def plannings_calendar(plannings) calendar = Icalendar::Calendar.new - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do plannings.each do |planning| create_timezone calendar planning.routes.select(&:vehicle_usage_id).each do |route| diff --git a/app/api/v01/plannings.rb b/app/api/v01/plannings.rb index bd4380beb..ace958c69 100644 --- a/app/api/v01/plannings.rb +++ b/app/api/v01/plannings.rb @@ -94,7 +94,7 @@ def planning_params requires :id, type: String, desc: SharedParams::ID_DESC end delete ':id' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first!.destroy status 204 end @@ -108,7 +108,7 @@ def planning_params requires :ids, type: Array[String], desc: 'Ids separated by comma. You can specify ref (not containing comma) instead of id, in this case you have to add "ref:" before each ref, e.g. ref:ref1,ref:ref2,ref:ref3.', coerce_with: CoerceArrayString end delete do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do Planning.transaction do current_customer.plannings.select{ |planning| params[:ids].any?{ |s| ParseIdsRefs.match(s, planning) } @@ -128,7 +128,7 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end patch ':id/refresh' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) @@ -152,7 +152,7 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end patch ':id/switch' do - Stop.includes_destinations.scoping do + Stop.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) @@ -186,7 +186,7 @@ def planning_params optional :out_of_zone, type: Boolean, desc: 'Take into account points out of zones.', default: true end patch ':id/automatic_insert' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).preload_route_details.first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) @@ -264,7 +264,7 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: '[Deprecated] Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring. Only active with synchronous option' end get ':id/optimize' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if planning.customer.job_optimizer @@ -296,7 +296,7 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end patch ':id/duplicate' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! planning = planning.duplicate planning.save! validate: Planner::Application.config.validate_during_duplication @@ -316,7 +316,7 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end patch ':id/order_array' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) @@ -382,7 +382,7 @@ def planning_params optional :with_details, type: Boolean, desc: 'Output route details', default: false end patch ':id/update_stops_status' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! if Job.on_planning(planning.customer.job_optimizer, planning.id) status 204 @@ -391,7 +391,7 @@ def planning_params service.fetch_stops_status(planning) planning.save! if params[:details] || params[:with_details] - present planning.routes.includes_destinations.available, with: V01::Entities::RouteStatus + present planning.routes.includes_destinations_and_stores.available, with: V01::Entities::RouteStatus else status 204 end @@ -409,7 +409,7 @@ def planning_params end get ':id/send_sms' do if current_customer.enable_sms && current_customer.reseller.messagings.any?{ |_k, v| v['enable'] == true } - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do send_sms_planning current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! end else diff --git a/app/api/v01/routes.rb b/app/api/v01/routes.rb index b9fba646a..0e1fd46ed 100644 --- a/app/api/v01/routes.rb +++ b/app/api/v01/routes.rb @@ -75,7 +75,7 @@ def get_route patch ':id/active/:active' do raise Exceptions::JobInProgressError if Job.on_planning(current_customer.job_optimizer, get_route.planning.id) - Stop.includes_destinations.scoping do + Stop.includes_destinations_and_stores.scoping do get_route.active(params[:active].to_s.to_sym) && get_route.compute_saved present get_route, with: V01::Entities::Route, geojson: params[:with_geojson] end @@ -98,7 +98,7 @@ def get_route id_hash[:ref] || id_hash[:id] }.compact planning_route_ids = Route.where(planning_id: get_route.planning.id).map(&:id) - Route.includes_destinations.where(id: get_route.id).scoping do + Route.includes_destinations_and_stores.where(id: get_route.id).scoping do visits_ordered = StopVisit .includes(:visit) .where(visits: { id: visit_ids }, route_id: planning_route_ids) @@ -136,7 +136,7 @@ def get_route begin raise Exceptions::JobInProgressError if current_customer.job_optimizer - Stop.includes_destinations.scoping do + Stop.includes_destinations_and_stores.scoping do if !Optimizer.optimize(get_route.planning, get_route, { global: false, synchronous: params[:synchronous], active_only: params[:all_stops].nil? ? params[:active_only] : !params[:all_stops], ignore_overload_multipliers: params[:ignore_overload_multipliers] }) status 304 else @@ -167,7 +167,7 @@ def get_route requires :id, type: String, desc: SharedParams::ID_DESC end patch ':id/reverse_order' do - Stop.includes_destinations.scoping do + Stop.includes_destinations_and_stores.scoping do raise Exceptions::JobInProgressError if Job.on_planning(current_customer.job_optimizer, get_route.planning.id) get_route && get_route.reverse_order && get_route.compute_saved! @@ -185,7 +185,7 @@ def get_route end get ':id/send_sms' do if current_customer.enable_sms && current_customer.reseller.messagings.any?{ |_k, v| v['enable'] == true } - Stop.includes_destinations.scoping do + Stop.includes_destinations_and_stores.scoping do send_sms_route get_route end else diff --git a/app/api/v01/stops.rb b/app/api/v01/stops.rb index 71f60e629..8d90d9767 100644 --- a/app/api/v01/stops.rb +++ b/app/api/v01/stops.rb @@ -99,7 +99,7 @@ def stop_params begin planning.move_stop(route, stop, Integer(params[:index])) planning.compute_saved - status 204 + status 204 rescue Exceptions::StopIndexError => e if e.route == route && e.bad_index == Integer(params[:index]) error! V01::Status.code_response(:code_400), 400 diff --git a/app/api/v100/plannings.rb b/app/api/v100/plannings.rb index ed5d8517b..a9bb28aa9 100644 --- a/app/api/v100/plannings.rb +++ b/app/api/v100/plannings.rb @@ -36,7 +36,7 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end patch ':id/automatic_insert' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) stops = planning.routes.flat_map{ |r| r.stops }.select{ |stop| params[:stop_ids].include?(stop.id) } @@ -77,7 +77,7 @@ def planning_params optional :active_only, type: Boolean, desc: 'Use only active stops.', default: true end patch ':id/optimized_insertion' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) diff --git a/app/api/v100/routes.rb b/app/api/v100/routes.rb index 447abfde2..e70abd71e 100644 --- a/app/api/v100/routes.rb +++ b/app/api/v100/routes.rb @@ -20,12 +20,12 @@ class V100::Routes < Grape::API requires :stop_ids, type: Array[Integer], desc: 'Ids separated by comma. You can specify ref (not containing comma) instead of id, in this case you have to add "ref:" before each ref, e.g. ref:ref1,ref:ref2,ref:ref3.', documentation: {param_type: 'form'}, coerce_with: CoerceArrayString end patch ':id/stops/moves' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:planning_id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) - route = planning.routes.includes_destinations.where(ParseIdsRefs.read(params[:id])).first! - moving_stops = planning.routes.includes_destinations.flat_map{ |r| r.stops }.select{ |stop| params[:stop_ids].include?(stop.id) } + route = planning.routes.includes_destinations_and_stores.where(ParseIdsRefs.read(params[:id])).first! + moving_stops = planning.routes.includes_destinations_and_stores.flat_map{ |r| r.stops }.select{ |stop| params[:stop_ids].include?(stop.id) } unless moving_stops.empty? begin Planning.transaction do @@ -57,16 +57,16 @@ class V100::Routes < Grape::API requires :visit_ids, types: [Array[String], Array[Integer]], desc: 'Ids separated by comma. You can specify ref (not containing comma) instead of id, in this case you have to add "ref:" before each ref, e.g. ref:ref1,ref:ref2,ref:ref3.', documentation: {param_type: 'form'}, coerce_with: CoerceArrayString end patch ':id/visits/moves' do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:planning_id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) - route = planning.routes.includes_destinations.where(ParseIdsRefs.read(params[:id])).first! + route = planning.routes.includes_destinations_and_stores.where(ParseIdsRefs.read(params[:id])).first! visit_ids = params[:visit_ids].map{ |raw_id| id_hash = ParseIdsRefs.read(raw_id) id_hash[:ref] || id_hash[:id] }.compact - moving_stops = planning.routes.includes_destinations.flat_map{ |r| r.stops }.select{ |stop| stop.is_a?(StopVisit) && visit_ids.include?(stop.visit.id) } + moving_stops = planning.routes.includes_destinations_and_stores.flat_map{ |r| r.stops }.select{ |stop| stop.is_a?(StopVisit) && visit_ids.include?(stop.visit.id) } unless moving_stops.empty? begin @@ -101,11 +101,11 @@ class V100::Routes < Grape::API post ':route_id/stores/:id' do error!(V100::Status.code_response(:code_401, after: I18n.t('errors.routes.enable_store_stops')), 401) if !current_customer.enable_store_stops - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:planning_id])).first! raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) - route = planning.routes.includes_destinations.where(ParseIdsRefs.read(params[:route_id])).first! + route = planning.routes.includes_destinations_and_stores.where(ParseIdsRefs.read(params[:route_id])).first! store = current_customer.stores.where(ParseIdsRefs.read(params[:id])).first! Planning.transaction do diff --git a/app/controllers/api_web/v01/plannings_controller.rb b/app/controllers/api_web/v01/plannings_controller.rb index 54bd89096..5df64d207 100644 --- a/app/controllers/api_web/v01/plannings_controller.rb +++ b/app/controllers/api_web/v01/plannings_controller.rb @@ -51,7 +51,7 @@ def manage_planning def includes_sub_models if action_name.to_sym == :print VehicleUsage.with_stores.scoping do - Route.includes_destinations.scoping do + Route.includes_destinations_and_stores.scoping do yield end end diff --git a/app/controllers/api_web/v01/routes_controller.rb b/app/controllers/api_web/v01/routes_controller.rb index 18b5b9cca..420ea713c 100644 --- a/app/controllers/api_web/v01/routes_controller.rb +++ b/app/controllers/api_web/v01/routes_controller.rb @@ -24,9 +24,9 @@ class ApiWeb::V01::RoutesController < ApiWeb::V01::ApiWebController def index @routes = if params.key?(:ids) ids = params[:ids].split(',') - @planning.routes.includes_destinations.where(ParseIdsRefs.where(Route, ids)).includes_vehicle_usages + @planning.routes.includes_destinations_and_stores.where(ParseIdsRefs.where(Route, ids)).includes_vehicle_usages else - @planning.routes.includes_destinations.includes_vehicle_usages + @planning.routes.includes_destinations_and_stores.includes_vehicle_usages end @layer = current_user.customer.profile.layers.find_by(id: params[:layer_id]) if params[:layer_id] @disable_clusters = ValueToBoolean.value_to_boolean(params[:disable_clusters], false) @@ -34,7 +34,7 @@ def index def print @params = params - @route = @planning.routes.includes_destinations.find(params[:id]) + @route = @planning.routes.includes_destinations_and_stores.find(params[:id]) respond_to do |format| format.html diff --git a/app/controllers/api_web/v01/zonings_controller.rb b/app/controllers/api_web/v01/zonings_controller.rb index 070cb5811..a741971fb 100644 --- a/app/controllers/api_web/v01/zonings_controller.rb +++ b/app/controllers/api_web/v01/zonings_controller.rb @@ -19,7 +19,7 @@ class ApiWeb::V01::ZoningsController < ApiWeb::V01::ApiWebController skip_before_action :verify_authenticity_token # because rails waits for a form token with POST load_and_authorize_resource before_action :manage_zoning - around_action :includes_destinations + around_action :includes_destinations_and_stores def edit capabilities @@ -55,8 +55,8 @@ def manage_zoning @manage_zoning = ApiWeb::V01::ZoningsController.manage end - def includes_destinations - Route.includes_destinations.scoping do + def includes_destinations_and_stores + Route.includes_destinations_and_stores.scoping do yield end end diff --git a/app/controllers/order_arrays_controller.rb b/app/controllers/order_arrays_controller.rb index a4058ab9a..21f4d95e2 100644 --- a/app/controllers/order_arrays_controller.rb +++ b/app/controllers/order_arrays_controller.rb @@ -33,7 +33,7 @@ def show if planning i = -1 - visit_index = Hash[planning.routes.includes_destinations.flat_map{ |route| + visit_index = Hash[planning.routes.includes_destinations_and_stores.flat_map{ |route| route.stops.select{ |stop| stop.is_a?(StopVisit) }.collect{ |stop| [stop.visit.id, route.vehicle_usage] } diff --git a/app/controllers/plannings_controller.rb b/app/controllers/plannings_controller.rb index 85908336d..f27d8e56a 100644 --- a/app/controllers/plannings_controller.rb +++ b/app/controllers/plannings_controller.rb @@ -30,6 +30,7 @@ class PlanningsController < ApplicationController before_action :set_planning, only: [:edit, :duplicate, :destroy, :cancel_optimize, :refresh, :route_edit] + UPDATE_ACTIONS before_action :set_planning_without_stops, only: [:data_header, :filter_routes, :modal, :sidebar, :refresh_route] before_action :set_driver_planning, only: [:driver_move] + before_action :set_device_definitions, only: [:edit, :update] before_action :check_no_existing_job, only: [:refresh, :driver_move] + UPDATE_ACTIONS around_action :over_max_limit, only: [:create, :duplicate] @@ -56,16 +57,16 @@ def index def show @params = params - @planning = current_user.customer.plannings.where(id: params[:id] || params[:planning_id]).includes(:routes).first! + @planning = current_user.customer.plannings.where(id: params[:id] || params[:planning_id]).preload_routes_without_stops.first! @routes = if params[:route_ids] route_ids = params[:route_ids].split(',').map{ |s| Integer(s) } @with_stops = true - @planning.routes.where(id: route_ids).includes_destinations.includes_vehicle_usages + @planning.routes.where(id: route_ids).includes_destinations_and_stores.includes_vehicle_usages else stops_count = 0 if @planning.routes.select{ |route| !route.hidden || !route.locked || route.vehicle_usage_id.nil? }.none?{ |r| (stops_count += r.stops_size) >= 1000 } @with_stops = true - @planning.routes.available.includes_destinations.includes_vehicle_usages + @planning.routes.available.includes_destinations_and_stores.includes_vehicle_usages else @with_stops = false @planning.routes.available.includes_vehicle_usages @@ -117,6 +118,19 @@ def edit @spreadsheet_columns = export_columns @with_devices = true capabilities + + # Prepare device definitions and related routes for the view to avoid business logic in the template + @device_definitions = @planning.customer.device.configured_definitions.each_with_object({}) do |(key, definition), hash| + # Only keep :deliver if SMS is enabled + next if key == :deliver && !@planning.customer.enable_sms + routes_with_configured_devices = @planning.routes.select do |route| + route.vehicle_usage_id && route.vehicle_usage.vehicle.devices.key?(definition[:device]) + end + hash[key] = { + definition: definition, + routes_with_configured_devices: routes_with_configured_devices + } + end end def create @@ -176,7 +190,8 @@ def driver_move def refresh respond_to do |format| if @planning.compute_saved - @routes = @planning.routes.includes_vehicle_usages.includes_destinations + @planning = Planning.where(id: @planning.id).preload_routes_without_stops.first! + @routes = @planning.routes.includes_vehicle_usages.includes_destinations_and_stores @with_devices = true format.json { render action: 'show', location: @planning } else @@ -186,14 +201,14 @@ def refresh end def refresh_route - @route = @planning.routes.where(id: params[:route_id]).includes_vehicle_usages.includes_destinations.first! + @route = @planning.routes.where(id: params[:route_id]).includes_vehicle_usages.includes_destinations_and_stores.first! stops_count = @route.stops.count page = params[:out_page] || 1 if @route.vehicle_usage_id current_route = @route - current_route.stops.includes_destinations.load + current_route.stops.includes_destinations_and_stores.load else - @out_pagy, @out_stops = pagy_countless(@route.stops.includes_destinations, page: page, page_param: :out_page) + @out_pagy, @out_stops = pagy_countless(@route.stops.includes_destinations_and_stores, page: page, page_param: :out_page) current_route = @route.dup current_route.stops = @out_stops end @@ -224,7 +239,7 @@ def sidebar @with_stops = @planning.routes.select{ |route| !route.hidden || !route.locked || route.vehicle_usage_id.nil? }.none?{ |r| (stops_count += r.stops_size) >= 1000 } @routes = if @with_stops - @planning.routes.includes_vehicle_usages.includes_destinations.available + @planning.routes.includes_vehicle_usages.includes_destinations_and_stores.available else @planning.routes.includes_vehicle_usages.available end @@ -269,7 +284,7 @@ def selection_details stops = Stop.joins(:route) .where(routes: { planning_id: planning.id }) .where(id: selected_stop_ids) - .includes_destinations + .includes_destinations_and_stores .only_stop_visits @selection_info[:stops_count] = stops.size @@ -327,7 +342,7 @@ def automatic_insert end if @planning.compute_saved && @planning.reload - @routes = @planning.routes.where(id: route_ids).includes_vehicle_usages.includes_destinations + @routes = @planning.routes.where(id: route_ids).includes_vehicle_usages.includes_destinations_and_stores format.json { render action: :show } else format.json { render json: @planning.errors, status: :unprocessable_entity } @@ -585,7 +600,23 @@ def ignore_overload_multipliers end def set_available_stores - @available_stores = current_user.customer.stores.map { |store| { id: store.id, name: store.name, ref: store.ref, icon: store.icon, color: store.color } } + @available_stores = current_user.customer.stores.pluck(:id, :name, :ref, :icon, :color).map do |id, name, ref, icon, color| + { id: id, name: name, ref: ref, icon: icon, color: color } + end + end + + def set_device_definitions + @device_definitions = @planning.customer.device.configured_definitions.each_with_object({}) do |(key, definition), hash| + # Only keep :deliver if SMS is enabled + next if key == :deliver && !@planning.customer.enable_sms + routes_with_configured_devices = @planning.routes.select do |route| + route.vehicle_usage_id && route.vehicle_usage.vehicle.devices.key?(definition[:device]) + end + hash[key] = { + definition: definition, + routes_with_configured_devices: routes_with_configured_devices + } + end end def set_planning_without_stops @@ -632,22 +663,6 @@ def set_planning @planning = current_user.customer.plannings.where(id: params[:id] || params[:planning_id]).preload_route_details.first! end - def includes_destinations - if @with_stops && (params[:route_id] || params[:route_ids] || [:automatic_insert, :optimize].include?(action_name.to_sym)) - # Preload only stops from necessary routes - Stop.includes_destinations.scoping do - yield - end - elsif @with_stops && ([:show, :edit].exclude?(action_name.to_sym) || !request.format.html?) - # Preload all stops from all routes - Route.includes_destinations.scoping do - yield - end - else - yield - end - end - def check_no_existing_job raise Exceptions::JobInProgressError if Job.on_planning(@planning.customer.job_optimizer, @planning.id) end diff --git a/app/controllers/routes_controller.rb b/app/controllers/routes_controller.rb index 896a085f7..d9474b6fc 100644 --- a/app/controllers/routes_controller.rb +++ b/app/controllers/routes_controller.rb @@ -34,7 +34,7 @@ class RoutesController < ApplicationController def mobile manage_planning @params = params - @stops = @route.stops.includes_destinations.only_active_stop_visits + @stops = @route.stops.includes_destinations_and_stores.only_active_stop_visits respond_to do |format| format.html { render 'routes/mobile', diff --git a/app/controllers/zones_controller.rb b/app/controllers/zones_controller.rb index bff9e4f2b..be8a3e6ab 100644 --- a/app/controllers/zones_controller.rb +++ b/app/controllers/zones_controller.rb @@ -2,7 +2,7 @@ class ZonesController < ApplicationController before_action :authenticate_user! before_action :set_zone, only: [:show] - around_action :includes_destinations, only: [:show] + around_action :includes_destinations_and_stores, only: [:show] load_and_authorize_resource @@ -43,8 +43,8 @@ def set_zone @zone = @zoning.zones.find(params[:id] || params[:zone_id]) end - def includes_destinations - Route.includes_destinations.scoping do + def includes_destinations_and_stores + Route.includes_destinations_and_stores.scoping do yield end end diff --git a/app/controllers/zonings_controller.rb b/app/controllers/zonings_controller.rb index 3b72fe4bd..552da68a0 100644 --- a/app/controllers/zonings_controller.rb +++ b/app/controllers/zonings_controller.rb @@ -21,7 +21,7 @@ class ZoningsController < ApplicationController before_action :set_planning, only: [:show, :edit, :new, :automatic, :from_planning] before_action :manage_zoning before_action :set_deliverable_unit_icons, only: [:edit, :automatic, :from_planning] - around_action :includes_destinations, only: [:show, :edit, :update, :automatic, :from_planning] + around_action :includes_destinations_and_stores, only: [:show, :edit, :update, :automatic, :from_planning] around_action :over_max_limit, only: [:create, :duplicate] load_and_authorize_resource @@ -198,8 +198,8 @@ def set_planning @planning = params.key?(:planning_id) && !params[:planning_id].empty? ? current_user.customer.plannings.preload_route_details.find(params[:planning_id]) : nil end - def includes_destinations - Route.includes_destinations.scoping do + def includes_destinations_and_stores + Route.includes_destinations_and_stores.scoping do yield end end diff --git a/app/helpers/plannings_helper.rb b/app/helpers/plannings_helper.rb index ae9487780..8f92fa95f 100644 --- a/app/helpers/plannings_helper.rb +++ b/app/helpers/plannings_helper.rb @@ -18,7 +18,7 @@ module PlanningsHelper def planning_vehicles_array(planning) customer = planning.customer - planning.vehicle_usage_set.vehicle_usages.active.map{ |vehicle_usage| + planning.vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| vehicle_usage.active? }.map{ |vehicle_usage| { id: vehicle_usage.vehicle.id, text: vehicle_usage.vehicle.name, @@ -31,7 +31,7 @@ def planning_vehicles_array(planning) def planning_vehicles_usages_map(planning) PlanningConcern.vehicles_usages_map(planning) - planning.vehicle_usage_set.vehicle_usages.active.each_with_object({}) do |vehicle_usage, hash| + planning.vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| vehicle_usage.active? }.each_with_object({}) do |vehicle_usage, hash| router_name = vehicle_usage.vehicle.default_router.name_locale[I18n.locale.to_s] || vehicle_usage.vehicle.default_router.name_locale[I18n.default_locale.to_s] || diff --git a/app/jobs/importer_destinations.rb b/app/jobs/importer_destinations.rb index 15624755c..971a061e8 100644 --- a/app/jobs/importer_destinations.rb +++ b/app/jobs/importer_destinations.rb @@ -975,7 +975,7 @@ def prepare_plannings(name, _options) attribute[:id] || @visit_index_to_id_hash[attribute[:visit_index]] } - visits = Visit.includes_destinations.where(id: visit_ids).index_by(&:id).values_at(*visit_ids) + visits = Visit.includes_destinations_and_stores.where(id: visit_ids).index_by(&:id).values_at(*visit_ids) store_ids = v[:visits].map{ |type, attribute, _active| next unless type == :store diff --git a/app/models/planning.rb b/app/models/planning.rb index f2ae389fe..4c53cb3b8 100644 --- a/app/models/planning.rb +++ b/app/models/planning.rb @@ -238,7 +238,7 @@ def visit_add(visit) def visit_filling visit_ids = routes.includes_stops.flat_map{ |route| route.stops.map{ |stop| stop.visit_id }} - Visit.includes_destinations.where(id: (customer.visit_ids - visit_ids)).select{ |visit| + Visit.includes_destinations_and_stores.where(id: (customer.visit_ids - visit_ids)).select{ |visit| tags_compatible?(visit.tags.to_a | visit.destination.tags.to_a) }.each{ |visit| visit_add(visit) } self.save! @@ -282,49 +282,20 @@ def compute_saved(options = {}) end def compute_saved!(options = {}) - stop_rests = [] - stop_visits = [] - stop_stores = [] - - # Collect all segments from all routes that need routing - all_segments = [] - computed_routes = [] - - routes.each{ |r| - if options[:bang]!= false || r.outdated && r.vehicle_usage? - computed_routes << r - segments = r.collect_segments_for_routing(r.stops) - all_segments << { route: r, segments: segments } if segments.any? - end - } + jobs_to_enqueue = [] - # Batch process all segments in parallel - precompute_traces(all_segments, options) - - computed_routes.each{ |r| - if options[:bang] == false - r.compute(options.merge(skip_preload: true)) - else - r.compute!(options.merge(skip_preload: true)) + if ActiveRecord::Base.connection.transaction_open? + compute_within_existing_transaction(options, jobs_to_enqueue) + else + Planning.transaction do + compute_within_existing_transaction(options, jobs_to_enqueue) end - stops_by_type = r.stops.group_by(&:type) - stop_visits += stops_by_type['StopVisit'].to_a.map(&:import_attributes) - stop_rests += stops_by_type['StopRest'].to_a.map(&:import_attributes) - stop_stores += stops_by_type['StopStore'].to_a.map(&:import_attributes) - - } - Route.import(computed_routes.map(&:import_attributes), validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) - StopVisit.import(stop_visits, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) - StopRest.import(stop_rests, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) - StopStore.import(stop_stores, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) + end - computed_routes.each{ |r| - r.invalidate_route_cache && r.reload - next unless Planner::Application.config.delayed_job_use + jobs_to_enqueue.each do |job| + Delayed::Job.enqueue(job) + end - Delayed::Job.enqueue(SimplifyGeojsonTracksJob.new(self.customer_id, r.id)) - } - self.save!(touch: false) && self.invalidate_planning_cache true end @@ -334,9 +305,9 @@ def precompute_traces(all_segments, options = {}) all_segments.each_slice(5) do |batch| batch.each do |route_data| threads << Thread.new do + route = route_data[:route] begin ActiveRecord::Base.connection_pool.with_connection do - route = route_data[:route] segments = route_data[:segments] router = route.vehicle_usage.vehicle.default_router router_options = route.vehicle_usage.vehicle.default_router_options.symbolize_keys @@ -585,13 +556,13 @@ def visits_compatibles def visits routes.flat_map{ |route| - route.stops.only_stop_visits.includes_destinations.map(&:visit) + route.stops.only_stop_visits.includes_destinations_and_stores.map(&:visit) } end def visits_to_stop_hash routes.flat_map{ |route| - route.stops.only_stop_visits.includes_destinations.map{ |stop| [stop.visit.id, stop] } + route.stops.only_stop_visits.includes_destinations_and_stores.map{ |stop| [stop.visit.id, stop] } }.to_h end @@ -780,7 +751,7 @@ def set_stops(optimum, **options) def fetch_stops_status Visit.transaction do if customer.enable_stop_status - stops_map = Hash[routes.includes_destinations.available.where.not(vehicle_usage_id: nil).flat_map(&:stops).map { |stop| [(stop.is_a?(StopVisit) ? "v#{stop.visit_id}" : "r#{stop.id}"), stop] }] + stops_map = Hash[routes.includes_destinations_and_stores.available.where.not(vehicle_usage_id: nil).flat_map(&:stops).map { |stop| [(stop.is_a?(StopVisit) ? "v#{stop.visit_id}" : "r#{stop.id}"), stop] }] routes.each(&:clear_eta_data) routes_quantities_changed = [] @@ -1004,6 +975,51 @@ def quantities private + def compute_within_existing_transaction(options = {}, jobs_to_enqueue = []) + stop_rests = [] + stop_visits = [] + stop_stores = [] + + # Collect all segments from all routes that need routing + all_segments = [] + computed_routes = [] + routes.each{ |r| + if options[:bang]!= false || r.outdated && r.vehicle_usage? + computed_routes << r + segments = r.collect_segments_for_routing(r.stops) + all_segments << { route: r, segments: segments } if segments.any? + end + } + + # Batch process all segments in parallel + precompute_traces(all_segments, options) + + computed_routes.each{ |r| + if options[:bang] == false + r.compute(options.merge(skip_preload: true)) + else + r.compute!(options.merge(skip_preload: true)) + end + stops_by_type = r.stops.group_by(&:type) + stop_visits += stops_by_type['StopVisit'].to_a.map(&:import_attributes) + stop_rests += stops_by_type['StopRest'].to_a.map(&:import_attributes) + stop_stores += stops_by_type['StopStore'].to_a.map(&:import_attributes) + } + Route.import(computed_routes.map(&:import_attributes), validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) + StopVisit.import(stop_visits, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) + StopRest.import(stop_rests, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) + StopStore.import(stop_stores, validate_with_context: :update, raise_error: true, on_duplicate_key_update: {conflict_target: [:id], columns: :all}) + + computed_routes.each{ |r| + r.invalidate_route_cache && r.reload + next unless Planner::Application.config.delayed_job_use + + jobs_to_enqueue << SimplifyGeojsonTracksJob.new(self.customer_id, r.id) + } + + self.save!(touch: false) && self.invalidate_planning_cache + end + def select_insertion_data(insertion_data) insertion_by_route = insertion_data.group_by { |data| data[0] } selected_insertions = [] @@ -1040,7 +1056,6 @@ def collect_insertion_data(route, stop, options = {}) previous_position.distance(stop.position) + stop.position.distance(s.position) - previous_position.distance(s.position) ] previous_position = s.position - } next_position = route.vehicle_usage.default_store_stop&.position || stop.position insertion_data << diff --git a/app/models/route.rb b/app/models/route.rb index 9f7deb24a..fa46a6814 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -65,7 +65,29 @@ class Route < ApplicationRecord } scope :includes_stops, -> { includes(:stops) } # The second visit is for counting the visit index from all the visits of the destination - scope :includes_destinations, -> { includes(stops: {visit: [:relation_currents, :relation_successors, :tags, destination: [:tags, :visits, { customer: :deliverable_units }]]}) } + scope :includes_destinations_and_stores, -> { + includes( + stops: [ + { + visit: [ + :relation_currents, + :relation_successors, + :tags, + destination: [ + :tags, + :visits, + { customer: :deliverable_units } + ] + ] + }, + { + store: [ + :customer + ] + } + ] + ) + } scope :includes_deliverable_units, -> { includes(vehicle_usage: [:vehicle_usage_set, vehicle: [customer: :deliverable_units]]) } scope :stop_visits, -> { includes(:stops).where(type: StopVisit.name) } @@ -1006,7 +1028,7 @@ def compute_out_of_relations end def preload_compute_scopes - Route.where(id: self.id).includes_vehicle_usages.includes_destinations.first + Route.where(id: self.id).includes_vehicle_usages.includes_destinations_and_stores.first end def import_attributes diff --git a/app/models/stop.rb b/app/models/stop.rb index 0527c0718..902816034 100644 --- a/app/models/stop.rb +++ b/app/models/stop.rb @@ -42,7 +42,21 @@ class Stop < ApplicationRecord scope :only_stop_visits, -> { where(type: StopVisit.name) } scope :only_stop_stores, -> { where(type: StopStore.name) } scope :only_active_stop_visits, -> { only_stop_visits.where(active: true) } - scope :includes_destinations, -> { includes(visit: [:tags, destination: [:visits, :tags, {customer: :deliverable_units}]]) } + scope :includes_destinations_and_stores, -> { + includes( + visit: [ + :tags, + destination: [ + :visits, + :tags, + { customer: :deliverable_units } + ] + ], + store: [ + :customer + ] + ) + } scope :includes_relations, -> { includes(visit: [:relation_currents, :relation_successors])} before_save :outdate_route diff --git a/app/models/visit.rb b/app/models/visit.rb index 671229ed8..ba05402e1 100644 --- a/app/models/visit.rb +++ b/app/models/visit.rb @@ -37,7 +37,7 @@ class Visit < ApplicationRecord validates :ref, uniqueness: { scope: :destination_id, case_sensitive: true }, allow_nil: true, allow_blank: true scope :positioned, -> { joins(:destination).merge(Destination.positioned) } - scope :includes_destinations, -> { includes([:tags, destination: :tags]) } + scope :includes_destinations_and_stores, -> { includes([:tags, destination: :tags]) } enum force_position: { neutral: 0, diff --git a/app/views/plannings/_edit.html.haml b/app/views/plannings/_edit.html.haml index a8daeba9e..a01b61a8b 100644 --- a/app/views/plannings/_edit.html.haml +++ b/app/views/plannings/_edit.html.haml @@ -139,13 +139,15 @@ %i.fa.fa-desktop.fa-fw = current_user.customer.external_callback_name || t('plannings.edit.customer_external_callback_url.action') %li.divider{role: "separator"} - - @planning.customer.device.configured_definitions.each do |key, definition| + - @device_definitions.each do |key, data| + - definition = data[:definition] + - routes_with_configured_devices = data[:routes_with_configured_devices] - if key == :deliver && @planning.customer.enable_sms %li = link_to planning_modal_path(@planning, modal: 'sms_drivers'), remote: true, format: 'js', 'data-toggle': "modal", 'data-target': "#planning-send-sms-drivers-modal", 'data-keyboard': "true", id: 'send-sms-drivers' do %i.fa.fa-fw.fa-comment-sms = t('plannings.edit.deliver_send.plural.send_sms') - - if @planning.routes.any?{ |route| route.vehicle_usage_id && !route.vehicle_usage.vehicle.devices.key?(definition[:device]) } + - if routes_with_configured_devices.any? - definition[:route_operations].each do |route_operation| - options = { planning_id: "#{@planning.id}" } - options.merge!({ toggle: 'tooltip', title: "#{t(plannings.edit.tomtom_send_waypoints.title).html_safe}" }) if route_operation.is_a?(Hash) && route_operation[:send].to_s == 'waypoints' @@ -274,8 +276,8 @@ }, zoning_ids: @planning.zonings.collect(&:id), map_layers: Hash[layers.map{ |l| l[:name] }.zip(layers)], - map_lat: @planning.vehicle_usage_set.vehicle_usages.active.collect(&:default_store_start).compact.find{ |s| s.lat }.try(:lat) || @planning.customer.default_position[:lat], - map_lng: @planning.vehicle_usage_set.vehicle_usages.active.collect(&:default_store_start).compact.find{ |s| s.lng }.try(:lng) || @planning.customer.default_position[:lng], + map_lat: @planning.vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| vehicle_usage.active? }.collect(&:default_store_start).compact.find{ |s| s.lat }.try(:lat) || @planning.customer.default_position[:lat], + map_lng: @planning.vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| vehicle_usage.active? }.collect(&:default_store_start).compact.find{ |s| s.lng }.try(:lng) || @planning.customer.default_position[:lng], vehicles_array: planning_vehicles_array(@planning), vehicles_usages_map: planning_vehicles_usages_map(@planning), quantities: planning_quantities(@planning), diff --git a/app/views/plannings/show.json.jbuilder b/app/views/plannings/show.json.jbuilder index 9d6b8233a..6d2844fec 100644 --- a/app/views/plannings/show.json.jbuilder +++ b/app/views/plannings/show.json.jbuilder @@ -18,12 +18,12 @@ else json.customer_enable_external_callback current_user.customer.enable_external_callback? json.customer_external_callback_name current_user.customer.external_callback_name json.customer_external_callback_url current_user.customer.external_callback_url - duration = @planning.routes.includes_vehicle_usages.select(&:vehicle_usage).to_a.sum(0){ |route| route.visits_duration.to_i + route.wait_time.to_i + route.drive_time.to_i + route.vehicle_usage.default_service_time_start.to_i + route.vehicle_usage.default_service_time_end.to_i} + duration = @planning.routes.select(&:vehicle_usage).to_a.sum(0){ |route| route.visits_duration.to_i + route.wait_time.to_i + route.drive_time.to_i + route.vehicle_usage.default_service_time_start.to_i + route.vehicle_usage.default_service_time_end.to_i} json.duration time_over_day(duration) json.distance locale_distance(@planning.routes.to_a.sum(0){ |route| route.distance || 0 }, current_user.prefered_unit) (json.outdated true) if @planning.outdated json.size @planning.routes.to_a.sum(0){ |route| route.stops_size } json.size_active @planning.cached_active_stops_sum - json.routes (@routes || (@with_stops ? @planning.routes.includes_vehicle_usages.includes_destinations.available : @planning.routes)), partial: 'routes/edit', formats: [:json], handlers: [:jbuilder], as: :route, locals: { list_devices: planning_devices(@planning.customer), stops_count: nil, planning: @planning } + json.routes (@routes || (@with_stops ? @planning.routes.includes_vehicle_usages.includes_destinations_and_stores.available : @planning.routes)), partial: 'routes/edit', formats: [:json], handlers: [:jbuilder], as: :route, locals: { list_devices: planning_devices(@planning.customer), stops_count: nil, planning: @planning } end diff --git a/app/views/routes/_index.csv.ruby b/app/views/routes/_index.csv.ruby index a7818f6c0..f9b51d186 100644 --- a/app/views/routes/_index.csv.ruby +++ b/app/views/routes/_index.csv.ruby @@ -1,4 +1,4 @@ - planning.routes.includes_destinations.includes_deliverable_units.select{ |route| route.stops_size > 0 }.select{ |route| + planning.routes.includes_destinations_and_stores.includes_deliverable_units.select{ |route| route.stops_size > 0 }.select{ |route| route.vehicle_usage_id || !@params.key?(:stops) || @params[:stops].split('|').include?('out-of-route') }.collect { |route| if summary diff --git a/app/views/routes/_index.excel.ruby b/app/views/routes/_index.excel.ruby index cd602c09b..2f1b0ce77 100644 --- a/app/views/routes/_index.excel.ruby +++ b/app/views/routes/_index.excel.ruby @@ -1,4 +1,4 @@ - planning.routes.includes_destinations.includes_deliverable_units.select{ |route| route.stops_size > 0 }.select{ |route| + planning.routes.includes_destinations_and_stores.includes_deliverable_units.select{ |route| route.stops_size > 0 }.select{ |route| route.vehicle_usage_id || !@params.key?(:stops) || @params[:stops].split('|').include?('out-of-route') }.collect { |route| if summary From e264715fd4ede8ae47747ecb6455cd1bc66f5212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 15:53:49 +0200 Subject: [PATCH 14/24] Apply new rubocop rules by default, update todo & remove some rules --- .rubocop.yml | 20 +- .rubocop_todo.yml | 741 +++++++++++++----- app/controllers/images_controller.rb | 2 +- app/models/route.rb | 2 +- config/routes.rb | 2 +- test/api/v01/plannings_test.rb | 18 +- test/api/v01/vehicle_usages_test.rb | 2 +- test/controllers/plannings_controller_test.rb | 4 +- test/lib/concave_hull_test.rb | 2 +- test/lib/routers/here_test.rb | 24 +- test/lib/routers/osrm_test.rb | 24 +- test/models/customer_test.rb | 4 +- test/models/vehicle_usage_test.rb | 8 +- test/models/zoning_test.rb | 4 +- test/test_helper.rb | 8 +- 15 files changed, 600 insertions(+), 265 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 066bc8d27..ea6eab060 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,12 @@ inherit_from: .rubocop_todo.yml -Metrics/LineLength: +AllCops: + NewCops: enable + +Layout/LineLength: Enabled: false -Style/SpaceBeforeBlockBraces: +Layout/SpaceBeforeBlockBraces: Enabled: false Metrics/MethodLength: @@ -39,7 +42,7 @@ Style/TrailingCommaInArguments: Style/MultilineBlockChain: Enabled: false -Style/SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: Enabled: false Style/NegatedIf: @@ -51,7 +54,7 @@ Layout/FirstHashElementIndentation: Style/NumericLiterals: Enabled: false -Style/LeadingCommentSpace: +Layout/LeadingCommentSpace: Enabled: false Style/SignalException: @@ -60,9 +63,6 @@ Style/SignalException: Style/IfUnlessModifier: Enabled: false -Style/CommentIndentation: - Enabled: false - Layout/FirstArrayElementIndentation: Enabled: false @@ -72,13 +72,13 @@ Style/ClassAndModuleChildren: Metrics/ClassLength: Enabled: false -Style/IndentationWidth: +Layout/IndentationWidth: Enabled: false -Lint/EndAlignment: +Layout/EndAlignment: Enabled: false -Style/ElseAlignment: +Layout/ElseAlignment: Enabled: false Style/BlockDelimiters: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ecc606549..cbf9def3f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-08-12 09:31:19 UTC using RuboCop version 1.65.1. +# on 2025-07-23 13:45:28 UTC using RuboCop version 1.78.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 33 +# Offense count: 37 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. # Include: **/*.gemfile, **/Gemfile, **/gems.rb @@ -22,47 +22,34 @@ Layout/AccessModifierIndentation: Exclude: - 'bin/bundle' -# Offense count: 252 +# Offense count: 267 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: with_first_argument, with_fixed_indentation Layout/ArgumentAlignment: Enabled: false -# Offense count: 19 +# Offense count: 13 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: with_first_element, with_fixed_indentation Layout/ArrayAlignment: Exclude: - - 'app/controllers/plannings_controller.rb' - - 'app/helpers/destinations_helper.rb' - - 'app/jobs/importer_destinations.rb' - 'app/views/api_web/v01/routes/index.json.jbuilder' - - 'app/views/routes/_show.csv.ruby' - 'lib/devices/alyacom.rb' - 'test/api/v01/destinations_test.rb' - 'test/api/v01/routes_test.rb' -# Offense count: 5 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IndentationWidth. Layout/AssignmentIndentation: Exclude: - 'app/jobs/importer_destinations.rb' - - 'app/models/planning.rb' - 'app/models/route.rb' - 'lib/devices/praxedo.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith, Severity. -# SupportedStylesAlignWith: start_of_line, begin -Layout/BeginEndAlignment: - Exclude: - - 'app/models/deliverable_unit_quantity.rb' - -# Offense count: 10 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleAlignWith. # SupportedStylesAlignWith: either, start_of_block, start_of_line @@ -71,18 +58,15 @@ Layout/BlockAlignment: - 'app/api/v01/plannings_get.rb' - 'app/jobs/importer_destinations.rb' - 'app/models/planning.rb' - - 'app/views/routes/_edit.json.jbuilder' - 'db/migrate/20130807200039_create_rails_admin_histories_table.rb' - 'lib/pi.rb' - - 'lib/simplify_geometry.rb' - 'test/api/v01/zonings_test.rb' - 'test/jobs/importer_destinations_test.rb' -# Offense count: 10 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). Layout/BlockEndNewline: Exclude: - - 'app/api/v01/destinations.rb' - 'app/api/v01/plannings_get.rb' - 'app/models/import_csv.rb' - 'lib/devices/tomtom.rb' @@ -135,25 +119,24 @@ Layout/DotPosition: - 'test/test_helper.rb' - 'test/views/plannings_test.rb' -# Offense count: 61 +# Offense count: 65 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLineAfterGuardClause: Enabled: false -# Offense count: 28 +# Offense count: 31 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Enabled: false -# Offense count: 15 +# Offense count: 14 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLines: Exclude: - 'Gemfile' - 'app/api/v01/stores.rb' - 'app/controllers/api_web/v01/routes_controller.rb' - - 'app/controllers/index_controller.rb' - 'app/models/router_osrm.rb' - 'app/models/visit.rb' - 'db/migrate/20240215103513_job_progress_as_jsonb.rb' @@ -178,14 +161,14 @@ Layout/EmptyLinesAroundBeginBody: Exclude: - 'app/api/v100/plannings.rb' -# Offense count: 24 +# Offense count: 31 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, no_empty_lines Layout/EmptyLinesAroundBlockBody: Enabled: false -# Offense count: 88 +# Offense count: 84 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only @@ -208,12 +191,27 @@ Layout/EmptyLinesAroundMethodBody: - 'db/migrate/20160914104336_create_table_deliverable_unit.rb' - 'test/lib/devices/teksat_base.rb' -# Offense count: 20 +# Offense count: 19 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines Layout/EmptyLinesAroundModuleBody: - Enabled: false + Exclude: + - 'app/api/v01/helper/deliverable_by_vehicles_helper.rb' + - 'app/helpers/customers_helper.rb' + - 'app/models/concerns/consistency.rb' + - 'app/models/concerns/time_attr.rb' + - 'test/lib/devices/alyacom_base.rb' + - 'test/lib/devices/api_base.rb' + - 'test/lib/devices/deliver_base.rb' + - 'test/lib/devices/masternaut_base.rb' + - 'test/lib/devices/notico_base.rb' + - 'test/lib/devices/orange_base.rb' + - 'test/lib/devices/praxedo_base.rb' + - 'test/lib/devices/sopac_base.rb' + - 'test/lib/devices/stg_telematics_base.rb' + - 'test/lib/devices/teksat_base.rb' + - 'test/lib/devices/tomtom_base.rb' # Offense count: 16 # This cop supports safe autocorrection (--autocorrect). @@ -234,21 +232,19 @@ Layout/ExtraSpacing: - 'test/models/customer_test.rb' - 'test/models/planning_test.rb' -# Offense count: 15 +# Offense count: 13 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses Layout/FirstArgumentIndentation: Exclude: - - 'app/api/v01/custom_attributes.rb' - 'app/api/v01/vehicle_usage_sets.rb' - - 'app/api/v01/vehicle_usages.rb' - 'app/api/v100/plannings.rb' - 'app/jobs/importer_destinations.rb' - 'db/seeds.rb' - 'test/models/import_csv_test.rb' -# Offense count: 39 +# Offense count: 36 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table @@ -256,7 +252,6 @@ Layout/FirstArgumentIndentation: # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit Layout/HashAlignment: Exclude: - - 'app/api/api_logger.rb' - 'app/api/v01/devices/device_helpers.rb' - 'app/controllers/application_controller.rb' - 'app/controllers/stops_controller.rb' @@ -269,7 +264,7 @@ Layout/HashAlignment: - 'test/models/customer_test.rb' - 'test/models/planning_test.rb' -# Offense count: 71 +# Offense count: 69 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: normal, indented_internal_methods @@ -280,7 +275,6 @@ Layout/IndentationConsistency: - 'app/models/planning.rb' - 'config/environments/production.rb' - 'db/migrate/20130807200039_create_rails_admin_histories_table.rb' - - 'lib/optim/optimizer_wrapper.rb' - 'lib/pi.rb' - 'test/models/customer_test.rb' @@ -310,6 +304,23 @@ Layout/LeadingEmptyLines: - 'db/migrate/20130807200021_create_plannings_tags_join_table.rb' - 'spec/capybara_helper.rb' +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: space, no_space +Layout/LineContinuationSpacing: + Exclude: + - 'lib/k_means_same_size.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: aligned, indented +Layout/LineEndStringConcatenationIndentation: + Exclude: + - 'lib/k_means_same_size.rb' + - 'spec/requests/api_web/v01/zones_spec.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -318,17 +329,16 @@ Layout/MultilineArrayBraceLayout: Exclude: - 'test/api/v01/devices/orange_test.rb' -# Offense count: 6 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). Layout/MultilineBlockLayout: Exclude: - - 'app/api/v01/destinations.rb' - 'app/api/v01/plannings_get.rb' - 'app/models/import_csv.rb' - 'lib/devices/tomtom.rb' - 'test/api/v01/devices/deliver_test.rb' -# Offense count: 5 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line @@ -336,18 +346,31 @@ Layout/MultilineHashBraceLayout: Exclude: - 'app/api/api_v01.rb' - 'app/api/api_v100.rb' - - 'lib/optim/optimizer_wrapper.rb' - 'test/api/v01/devices/orange_test.rb' - 'test/api/v01/vehicles_test.rb' -# Offense count: 27 +# Offense count: 24 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineMethodCallBraceLayout: - Enabled: false + Exclude: + - 'app/api/api_v01.rb' + - 'app/api/api_v100.rb' + - 'app/api/v01/customers.rb' + - 'app/api/v01/vehicle_usage_sets.rb' + - 'app/controllers/vehicle_usages_controller.rb' + - 'app/jobs/importer_destinations.rb' + - 'app/views/routes/_show.csv.ruby' + - 'db/seeds.rb' + - 'lib/routers/here.rb' + - 'test/api/v01/devices/deliver_test.rb' + - 'test/api/v01/devices/fleet_reporting_test.rb' + - 'test/api/v01/devices/praxedo_test.rb' + - 'test/api/v01/devices/teksat_test.rb' + - 'test/models/zoning_test.rb' -# Offense count: 28 +# Offense count: 26 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver @@ -363,7 +386,7 @@ Layout/MultilineMethodCallIndentation: - 'test/test_helper.rb' - 'test/views/plannings_test.rb' -# Offense count: 30 +# Offense count: 33 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented @@ -375,16 +398,14 @@ Layout/MultilineOperationIndentation: - 'app/views/vehicle_usage_sets/show.csv.ruby' - 'app/views/vehicle_usage_sets/show.excel.ruby' - 'lib/optim/optimizer_wrapper.rb' - - 'test/models/planning_test.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Layout/RescueEnsureAlignment: Exclude: - - 'app/models/deliverable_unit_quantity.rb' - 'lib/devices/orange.rb' -# Offense count: 9 +# Offense count: 13 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAfterColon: Exclude: @@ -393,7 +414,7 @@ Layout/SpaceAfterColon: - 'test/jobs/importer_stores_test.rb' - 'test/models/application_record_test.rb' -# Offense count: 36 +# Offense count: 43 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAfterComma: Exclude: @@ -433,7 +454,7 @@ Layout/SpaceAroundMethodCallOperator: Exclude: - 'test/api/v01/users_test.rb' -# Offense count: 148 +# Offense count: 194 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. # SupportedStylesForExponentOperator: space, no_space @@ -461,7 +482,7 @@ Layout/SpaceBeforeSemicolon: - 'db/migrate/20160314100318_alter_table_router_dimensions.rb' - 'test/api/v01/devices/tomtom_test.rb' -# Offense count: 18 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_no_space, require_space @@ -471,19 +492,17 @@ Layout/SpaceInLambdaLiteral: - 'config/routes.rb' - 'test/controllers/destinations_controller_test.rb' - 'test/controllers/plannings_by_destinations_controller_test.rb' - - 'test/controllers/plannings_controller_test.rb' - 'test/models/customer_test.rb' - 'test/models/destination_test.rb' - 'test/models/visit_test.rb' -# Offense count: 41 +# Offense count: 39 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. # SupportedStyles: space, no_space, compact # SupportedStylesForEmptyBrackets: space, no_space Layout/SpaceInsideArrayLiteralBrackets: Exclude: - - 'app/api/v01/customers.rb' - 'config/initializers/devise.rb' - 'lib/devices/alyacom.rb' - 'lib/devices/orange.rb' @@ -492,7 +511,7 @@ Layout/SpaceInsideArrayLiteralBrackets: - 'test/lib/devices/masternaut_base.rb' - 'test/lib/devices/orange_base.rb' -# Offense count: 77 +# Offense count: 89 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space @@ -500,7 +519,7 @@ Layout/SpaceInsideArrayLiteralBrackets: Layout/SpaceInsideBlockBraces: Enabled: false -# Offense count: 39 +# Offense count: 41 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: space, compact, no_space @@ -516,7 +535,7 @@ Layout/SpaceInsideStringInterpolation: - 'app/mailers/route_mailer.rb' - 'test/mailers/user_mailer_test.rb' -# Offense count: 5 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: final_newline, final_blank_line @@ -524,30 +543,27 @@ Layout/TrailingEmptyLines: Exclude: - 'app/api/v01/entities/vehicle_temperature.rb' - 'app/api/v01/helper/plannings_helper_api.rb' - - 'app/services/sopac_service.rb' - 'db/migrate/20130807200016_create_destinations_tags_join_table.rb' - 'db/migrate/20130807200021_create_plannings_tags_join_table.rb' -# Offense count: 3 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowInHeredoc. Layout/TrailingWhitespace: Exclude: - 'db/migrate/20160201165010_split_table_destination_to_visit.rb' - 'db/migrate/20161220100839_add_missing_foreign_keys.rb' - - 'lib/devices/orange.rb' -# Offense count: 69 +# Offense count: 120 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Enabled: false -# Offense count: 22 +# Offense count: 17 # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperator: Exclude: - - 'test/api/v01/deliverable_units_test.rb' - 'test/api/v01/destinations_test.rb' - 'test/api/v01/devices/alyacom_test.rb' - 'test/api/v01/devices/fleet_demo_test.rb' @@ -557,10 +573,24 @@ Lint/AmbiguousOperator: - 'test/api/v01/devices/tomtom_test.rb' - 'test/api/v01/helpers/planning_icalendar_test.rb' - 'test/api/v01/visits_test.rb' - - 'test/jobs/importer_destinations_test.rb' - - 'test/models/deliverable_unit_test.rb' - 'test/models/planning_test.rb' - - 'test/models/visit_test.rb' + +# Offense count: 106 +# This cop supports safe autocorrection (--autocorrect). +Lint/AmbiguousOperatorPrecedence: + Enabled: false + +# Offense count: 10 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: RequireParenthesesForMethodChains. +Lint/AmbiguousRange: + Exclude: + - 'app/jobs/clustering.rb' + - 'app/models/concerns/time_attr.rb' + - 'app/models/customer.rb' + - 'app/models/router.rb' + - 'app/models/vehicle_usage.rb' + - 'test/api/v01/destinations_test.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). @@ -568,19 +598,13 @@ Lint/AmbiguousRegexpLiteral: Exclude: - 'test/views/indexes_test.rb' -# Offense count: 4 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Exclude: - 'app/controllers/zones_controller.rb' - 'lib/devices/stg_telematics.rb' - - 'lib/notifications.rb' - -# Offense count: 2 -Lint/BinaryOperatorWithIdenticalOperands: - Exclude: - - 'app/jobs/importer_destinations.rb' # Offense count: 63 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -598,6 +622,16 @@ Lint/BooleanSymbol: - 'app/api/v100/entities/route_properties.rb' - 'app/api/v100/plannings.rb' +# Offense count: 7 +# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch. +Lint/DuplicateBranch: + Exclude: + - 'app/controllers/plannings_controller.rb' + - 'app/helpers/application_helper.rb' + - 'app/models/route.rb' + - 'lib/routers/router_wrapper.rb' + - 'test/lib/devices/praxedo_base.rb' + # Offense count: 6 Lint/DuplicateMethods: Exclude: @@ -605,6 +639,17 @@ Lint/DuplicateMethods: - 'app/models/import_json.rb' - 'app/models/import_tomtom.rb' +# Offense count: 6 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'app/api/v01/customers.rb' + - 'app/api/v01/devices/trimble.rb' + - 'app/api/v01/jobs.rb' + - 'app/controllers/order_arrays_controller.rb' + - 'spec/features/login_spec.rb' + - 'test/api/v01/geocoder_test.rb' + # Offense count: 1 # Configuration parameters: AllowComments. Lint/EmptyWhen: @@ -637,6 +682,11 @@ Lint/IneffectiveAccessModifier: - 'app/models/route.rb' - 'lib/simplify_geometry.rb' +# Offense count: 1 +Lint/LiteralAssignmentInCondition: + Exclude: + - 'app/jobs/importer_destinations.rb' + # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/Loop: @@ -644,25 +694,35 @@ Lint/Loop: - 'lib/devices/alyacom.rb' - 'lib/devices/praxedo.rb' -# Offense count: 3 +# Offense count: 1 # Configuration parameters: AllowedParentClasses. Lint/MissingSuper: Exclude: - - 'app/models/deliverable_unit_operation.rb' - - 'app/models/deliverable_unit_quantity.rb' - 'lib/devices/fleet_demo.rb' # Offense count: 1 -# Configuration parameters: AllowedMethods, AllowedPatterns. -Lint/NestedMethodDefinition: +Lint/NoReturnInBeginEndBlocks: Exclude: - - 'test/test_helper.rb' + - 'app/models/concerns/quantity_attr.rb' # Offense count: 1 Lint/NonLocalExitFromIterator: Exclude: - 'app/controllers/stores_controller.rb' +# Offense count: 11 +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/OrAssignmentToConstant: + Exclude: + - 'app/jobs/geocoder_destinations_job.rb' + - 'app/jobs/geocoder_job.rb' + - 'app/jobs/geocoder_stores_job.rb' + - 'app/jobs/optimizer_job.rb' + - 'app/jobs/simplify_geojson_tracks_job.rb' + - 'lib/devices/alyacom.rb' + - 'lib/devices/fleet.rb' + - 'lib/font_awesome.rb' + # Offense count: 6 # This cop supports safe autocorrection (--autocorrect). Lint/ParenthesesAsGroupedExpression: @@ -673,12 +733,6 @@ Lint/ParenthesesAsGroupedExpression: - 'app/views/stops/_show.json.jbuilder' - 'db/migrate/20160201165010_split_table_destination_to_visit.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/RedundantRequireStatement: - Exclude: - - 'test/controllers/api_web/v01/zonings_controller_test.rb' - # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods. @@ -721,6 +775,7 @@ Lint/ScriptPermission: - 'public/api/0.1/examples/ruby/example.rb' # Offense count: 1 +# Configuration parameters: AllowRBSInlineAnnotation. Lint/SelfAssignment: Exclude: - 'db/migrate/20180219090520_change_hstore_to_jsonb.rb' @@ -732,10 +787,6 @@ Lint/ShadowedArgument: - 'lib/devices/fleet_demo.rb' - 'lib/devices/masternaut.rb' -# Offense count: 52 -Lint/ShadowingOuterLocalVariable: - Enabled: false - # Offense count: 6 # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: @@ -745,7 +796,19 @@ Lint/SuppressedException: - 'app/jobs/importer_destinations.rb' - 'app/jobs/importer_stores.rb' - 'lib/devices/masternaut.rb' - - 'lib/notifications.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/SuppressedExceptionInNumberConversion: + Exclude: + - 'app/models/planning.rb' + +# Offense count: 84 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: strict, consistent +Lint/SymbolConversion: + Enabled: false # Offense count: 1 # Configuration parameters: AllowKeywordBlockArguments. @@ -753,15 +816,16 @@ Lint/UnderscorePrefixedVariableName: Exclude: - 'app/jobs/importer_destinations.rb' -# Offense count: 292 +# Offense count: 320 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Enabled: false -# Offense count: 42 +# Offense count: 50 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. +# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Enabled: false @@ -773,12 +837,22 @@ Lint/UselessAccessModifier: - 'app/jobs/clustering.rb' - 'lib/simplify_geometry.rb' -# Offense count: 61 -# This cop supports unsafe autocorrection (--autocorrect-all). +# Offense count: 53 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect. Lint/UselessAssignment: Enabled: false +# Offense count: 1 +Lint/UselessOr: + Exclude: + - 'app/jobs/importer_destinations.rb' + +# Offense count: 1 +Lint/UselessRescue: + Exclude: + - 'lib/geocode_addok_wrapper.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect, CheckForMethodsWithNoSideEffects. @@ -786,43 +860,46 @@ Lint/Void: Exclude: - 'test/lib/devices/praxedo_test.rb' -# Offense count: 221 +# Offense count: 275 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 380 + Max: 336 -# Offense count: 29 +# Offense count: 32 # Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: Max: 5 -# Offense count: 4 +# Offense count: 1 +# Configuration parameters: LengthThreshold. +Metrics/CollectionLiteralLength: + Exclude: + - 'lib/font_awesome.rb' + +# Offense count: 6 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 247 + Max: 133 -# Offense count: 21 +# Offense count: 25 # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: Max: 8 MaxOptionalParameters: 5 -# Offense count: 12 +# Offense count: 2 Naming/AccessorMethodName: Exclude: - 'app/api/v01/routes.rb' - 'app/models/route.rb' - - 'app/services/deliver_service.rb' - - 'app/services/fleet_demo_service.rb' - - 'app/services/fleet_service.rb' - - 'app/services/masternaut_service.rb' - - 'app/services/orange_service.rb' - - 'app/services/stg_telematics_service.rb' - - 'app/services/suivi_de_flotte_service.rb' - - 'app/services/teksat_service.rb' - - 'app/services/tomtom_service.rb' - - 'lib/devices/fleet.rb' + +# Offense count: 25 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, BlockForwardingName. +# SupportedStyles: anonymous, explicit +Naming/BlockForwarding: + Enabled: false # Offense count: 3 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. @@ -845,14 +922,15 @@ Naming/MemoizedInstanceVariableName: - 'lib/devices/tomtom.rb' # Offense count: 5 -# Configuration parameters: EnforcedStyle, AllowedPatterns. +# Configuration parameters: EnforcedStyle, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. # SupportedStyles: snake_case, camelCase +# ForbiddenIdentifiers: __id__, __send__ Naming/MethodName: Exclude: - 'lib/devices/alyacom.rb' - 'lib/devices/masternaut.rb' -# Offense count: 16 +# Offense count: 14 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: @@ -864,20 +942,41 @@ Naming/MethodParameterName: - 'app/jobs/clustering.rb' - 'app/models/concerns/ref_sanitizer.rb' - 'app/models/customer.rb' - - 'app/models/route.rb' - 'app/models/zoning.rb' - 'lib/concave_hull.rb' - 'lib/devices/masternaut.rb' - 'lib/geocode_addok_wrapper.rb' - 'lib/pi.rb' -# Offense count: 8 -# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. -# NamePrefix: is_, has_, have_ -# ForbiddenPrefixes: is_, has_, have_ +# Offense count: 31 +# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. +# AllowedMethods: call +# WaywardPredicates: nonzero? +Naming/PredicateMethod: + Exclude: + - 'app/helpers/application_helper.rb' + - 'app/helpers/customers_helper.rb' + - 'app/jobs/importer_stores.rb' + - 'app/jobs/job.rb' + - 'app/models/customer.rb' + - 'app/models/destination.rb' + - 'app/models/planning.rb' + - 'app/models/route.rb' + - 'app/models/store.rb' + - 'app/models/visit.rb' + - 'app/models/zone.rb' + - 'app/services/messagings/sms_partner_service.rb' + - 'lib/devices/deliver.rb' + - 'lib/devices/fleet_demo.rb' + - 'test/integration/browser_benchmark_test.rb' + +# Offense count: 9 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. +# NamePrefix: is_, has_, have_, does_ +# ForbiddenPrefixes: is_, has_, have_, does_ # AllowedMethods: is_a? # MethodDefinitionMacros: define_method, define_singleton_method -Naming/PredicateName: +Naming/PredicatePrefix: Exclude: - 'spec/**/*' - 'app/helpers/customers_helper.rb' @@ -898,7 +997,7 @@ Naming/RescuedExceptionsVariableName: - 'lib/devices/trimble.rb' # Offense count: 32 -# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns. +# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. # SupportedStyles: snake_case, camelCase Naming/VariableName: Exclude: @@ -912,10 +1011,10 @@ Naming/VariableName: - 'test/lib/optim/optimizer_wrapper_test.rb' - 'test/test_helper.rb' -# Offense count: 664 +# Offense count: 711 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 Naming/VariableNumber: Enabled: false @@ -924,18 +1023,18 @@ Security/Eval: Exclude: - 'lib/k_means_same_size.rb' +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +Security/IoMethods: + Exclude: + - 'app/controllers/concerns/planning_export.rb' + # Offense count: 2 Security/MarshalLoad: Exclude: - 'app/controllers/concerns/import_export_customer.rb' - 'test/controllers/concerns/import_export_customer_test.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Security/YAMLLoad: - Exclude: - - 'app/api/v01/plannings_get.rb' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -952,6 +1051,26 @@ Style/Alias: Exclude: - 'config/initializers/rest_client.rb' +# Offense count: 10 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames. +# RedundantRestArgumentNames: args, arguments +# RedundantKeywordRestArgumentNames: kwargs, options, opts +# RedundantBlockArgumentNames: blk, block, proc +Style/ArgumentsForwarding: + Exclude: + - 'app/controllers/application_controller.rb' + - 'app/models/router_here.rb' + - 'app/models/router_otp.rb' + - 'app/models/types/serializable.rb' + - 'config/initializers/rest_client.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/ArrayIntersect: + Exclude: + - 'app/models/planning.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/BlockComments: @@ -985,7 +1104,7 @@ Style/ClassEqualityComparison: Exclude: - 'app/models/zone.rb' -# Offense count: 30 +# Offense count: 33 Style/ClassVars: Exclude: - 'app/jobs/importer_destinations.rb' @@ -999,7 +1118,24 @@ Style/ClassVars: - 'lib/value_to_boolean.rb' - 'test/api/v01/helpers/planning_icalendar_test.rb' -# Offense count: 11 +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedReceivers. +Style/CollectionCompact: + Exclude: + - 'app/models/customer.rb' + - 'app/models/planning.rb' + - 'app/models/route.rb' + +# Offense count: 5 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CollectionQuerying: + Exclude: + - 'app/models/route.rb' + - 'test/i18n/i18n_test.rb' + - 'test/models/planning_test.rb' + +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). Style/ColonMethodCall: Exclude: @@ -1008,13 +1144,13 @@ Style/ColonMethodCall: - 'db/migrate/20160415094723_update_table_store_icon.rb' - 'test/lib/concave_hull_test.rb' -# Offense count: 1 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'app/models/planning.rb' -# Offense count: 13 +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Keywords, RequireColon. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE @@ -1023,19 +1159,24 @@ Style/CommentAnnotation: - 'Gemfile' - 'app/api/v01/destinations.rb' - 'app/models/customer.rb' - - 'app/models/route.rb' - 'lib/devices/tomtom.rb' - 'lib/routers/osrm.rb' - 'lib/routers/otp.rb' - 'public/api/0.1/examples/ruby/example.rb' - 'test/api/v01/devices/fleet_demo_test.rb' -# Offense count: 3 +# Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CommentedKeyword: Exclude: - 'app/api/v01/devices/device_helpers.rb' - - 'app/models/route.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/ComparableClamp: + Exclude: + - 'app/models/planning.rb' + - 'lib/concave_hull.rb' # Offense count: 13 # This cop supports safe autocorrection (--autocorrect). @@ -1056,7 +1197,7 @@ Style/ConditionalAssignment: - 'lib/devices/tomtom.rb' - 'lib/optim/optimizer_wrapper.rb' -# Offense count: 448 +# Offense count: 510 # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false @@ -1067,16 +1208,15 @@ Style/EachWithObject: Exclude: - 'app/api/v01/helper/deliverable_by_vehicles_helper.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect, EnforcedStyle, AllowComments. # SupportedStyles: empty, nil, both Style/EmptyElse: Exclude: - - 'app/models/deliverable_unit_operation.rb' - 'app/models/stop.rb' -# Offense count: 4 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). Style/EmptyLiteral: Exclude: @@ -1105,6 +1245,16 @@ Style/EmptyMethod: - 'db/migrate/20170516093305_move_trace_to_route.rb' - 'test/test_helper.rb' +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: trailing_conditional, ternary +Style/EmptyStringInsideInterpolation: + Exclude: + - 'app/api/v01/vehicles.rb' + - 'app/helpers/custom_attributes_helper.rb' + - 'app/mailers/route_mailer.rb' + # Offense count: 30 # This cop supports safe autocorrection (--autocorrect). Style/ExpandPathArguments: @@ -1121,11 +1271,44 @@ Style/ExpandPathArguments: - 'test/models/zoning_test.rb' - 'test/test_helper.rb' -# Offense count: 33 +# Offense count: 35 # This cop supports safe autocorrection (--autocorrect). Style/ExplicitBlockArgument: Enabled: false +# Offense count: 31 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedVars, DefaultToNil. +Style/FetchEnvVar: + Exclude: + - 'bin/bundle' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/environments/test.rb' + - 'config/initializers/rest_client.rb' + - 'db/seeds.rb' + - 'docker/development.rb' + - 'docker/production.rb' + - 'lib/devices/masternaut.rb' + - 'lib/devices/praxedo.rb' + - 'lib/devices/suivi_de_flotte.rb' + - 'lib/devices/tomtom.rb' + - 'lib/devices/trimble.rb' + +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/FileNull: + Exclude: + - 'test/integration/browser_benchmark_test.rb' + - 'test/models/import_benchmark_test.rb' + - 'test/test_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/FileWrite: + Exclude: + - 'app/controllers/customers_controller.rb' + # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. @@ -1143,7 +1326,7 @@ Style/FormatString: # Offense count: 67 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: Exclude: @@ -1163,7 +1346,7 @@ Style/FormatStringToken: - 'test/lib/devices/masternaut_base.rb' - 'test/lib/devices/orange_base.rb' -# Offense count: 869 +# Offense count: 947 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -1176,7 +1359,7 @@ Style/GlobalVars: Exclude: - 'test/test_helper.rb' -# Offense count: 144 +# Offense count: 141 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: @@ -1197,14 +1380,19 @@ Style/HashAsLastArrayItem: - 'app/models/stop.rb' - 'app/models/visit.rb' -# Offense count: 26 +# Offense count: 64 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowSplatArgument. +Style/HashConversion: + Enabled: false + +# Offense count: 25 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedReceivers. # AllowedReceivers: Thread.current Style/HashEachMethods: Exclude: - 'app/api/v01/vehicles.rb' - - 'app/controllers/destinations_controller.rb' - 'app/controllers/vehicle_usages_controller.rb' - 'app/helpers/plannings_helper.rb' - 'app/helpers/vehicles_helper.rb' @@ -1215,11 +1403,16 @@ Style/HashEachMethods: - 'app/models/planning.rb' - 'app/models/route.rb' - 'app/models/vehicle.rb' - - 'app/models/visit.rb' - 'app/views/vehicle_usage_sets/show.csv.ruby' - 'app/views/vehicle_usage_sets/show.excel.ruby' -# Offense count: 70 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashExcept: + Exclude: + - 'config/application.rb' + +# Offense count: 72 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -1258,20 +1451,51 @@ Style/IfUnlessModifierOfIfUnless: - 'app/helpers/units_helper.rb' - 'lib/distance_units.rb' -# Offense count: 27 +# Offense count: 9 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods. +# AllowedMethods: nonzero? +Style/IfWithBooleanLiteralBranches: + Exclude: + - 'app/api/v01/entities/planning.rb' + - 'app/api/v01/entities/route.rb' + - 'app/api/v01/entities/route_properties.rb' + - 'app/api/v01/plannings_get.rb' + - 'app/api/v01/routes_get.rb' + - 'app/api/v100/entities/route.rb' + - 'app/api/v100/entities/route_properties.rb' + +# Offense count: 31 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Enabled: false -# Offense count: 99 +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/KeywordArgumentsMerging: + Exclude: + - 'lib/optim/optimizer_wrapper.rb' + +# Offense count: 106 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: line_count_dependent, lambda, literal Style/Lambda: Enabled: false -# Offense count: 80 +# Offense count: 8 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: + Exclude: + - 'app/api/v01/plannings.rb' + - 'app/controllers/plannings_by_destinations_controller.rb' + - 'app/helpers/customers_helper.rb' + - 'app/jobs/importer_destinations.rb' + - 'lib/optim/optimizer_wrapper.rb' + - 'test/models/planning_test.rb' + +# Offense count: 97 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. Style/MethodCallWithoutArgsParentheses: @@ -1314,16 +1538,12 @@ Style/MixinUsage: - 'test/controllers/order_arrays_controller_test.rb' - 'test/controllers/plannings_controller_test.rb' -# Offense count: 23 +# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). Style/MultilineIfModifier: Exclude: - 'app/api/v01/destinations.rb' - - 'app/api/v01/entities/visit.rb' - - 'app/api/v01/visits_get.rb' - - 'app/api/v100/entities/visit.rb' - 'app/controllers/api_web/v01/zonings_controller.rb' - - 'app/controllers/destinations_controller.rb' - 'app/controllers/zonings_controller.rb' - 'app/models/route.rb' - 'app/models/vehicle.rb' @@ -1338,7 +1558,7 @@ Style/MultilineIfModifier: Style/MultilineTernaryOperator: Enabled: false -# Offense count: 5 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMethodComparison, ComparisonsThreshold. Style/MultipleComparison: @@ -1348,6 +1568,9 @@ Style/MultipleComparison: - 'app/models/vehicle.rb' - 'app/models/visit.rb' - 'db/migrate/20180219090520_change_hstore_to_jsonb.rb' + - 'lib/devices/device_base.rb' + - 'lib/devices/masternaut.rb' + - 'test/api/v01/devices/fleet_test.rb' # Offense count: 15 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1366,20 +1589,25 @@ Style/MutableConstant: - 'lib/optim/optimizer_wrapper.rb' - 'public/api/0.1/examples/ruby/example.rb' +# Offense count: 43 +# This cop supports safe autocorrection (--autocorrect). +Style/NegatedIfElseCondition: + Enabled: false + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/NegatedWhile: Exclude: - 'lib/k_means_same_size.rb' -# Offense count: 83 +# Offense count: 87 # This cop supports safe autocorrection (--autocorrect). Style/NestedTernaryOperator: Enabled: false -# Offense count: 41 +# Offense count: 39 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinBodyLength. +# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals. # SupportedStyles: skip_modifier_ifs, always Style/Next: Enabled: false @@ -1392,6 +1620,12 @@ Style/NilComparison: Exclude: - 'app/models/planning.rb' +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/NilLambda: + Exclude: + - 'test/controllers/plannings_controller_test.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/Not: @@ -1406,13 +1640,24 @@ Style/NumericLiteralPrefix: Exclude: - 'test/models/visit_test.rb' -# Offense count: 139 +# Offense count: 170 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false +# Offense count: 14 +Style/OpenStructUse: + Exclude: + - 'app/api/v01/destinations.rb' + - 'app/api/v01/stores.rb' + - 'app/api/v01/zonings.rb' + - 'app/controllers/api_web/v01/stores_controller.rb' + - 'config/application.rb' + - 'config/environments/test.rb' + - 'test/services/messagings/messaging_service_test.rb' + # Offense count: 4 Style/OptionalArguments: Exclude: @@ -1421,7 +1666,7 @@ Style/OptionalArguments: - 'lib/devices/suivi_de_flotte.rb' - 'lib/devices/trimble.rb' -# Offense count: 44 +# Offense count: 46 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: @@ -1436,8 +1681,6 @@ Style/OptionalBooleanParameter: - 'app/models/zoning.rb' - 'lib/devices/fleet.rb' - 'lib/devices/tomtom.rb' - - 'lib/notifications.rb' - - 'lib/simplify_geometry.rb' - 'lib/value_to_boolean.rb' # Offense count: 30 @@ -1470,7 +1713,7 @@ Style/ParenthesesAroundCondition: - 'test/api/v01/plannings_test.rb' - 'test/models/planning_test.rb' -# Offense count: 24 +# Offense count: 26 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -1488,13 +1731,33 @@ Style/Proc: Exclude: - 'config/application.rb' -# Offense count: 141 +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: same_as_string_literals, single_quotes, double_quotes +Style/QuotedSymbols: + Exclude: + - 'lib/devices/masternaut.rb' + - 'test/models/import_csv_test.rb' + +# Offense count: 153 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowedCompactTypes. # SupportedStyles: compact, exploded Style/RaiseArgs: Enabled: false +# Offense count: 32 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Methods. +Style/RedundantArgument: + Enabled: false + +# Offense count: 51 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantArrayConstructor: + Enabled: false + # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Style/RedundantAssignment: @@ -1503,13 +1766,15 @@ Style/RedundantAssignment: - 'app/models/planning.rb' - 'test/jobs/importer_vehicle_usage_sets_test.rb' -# Offense count: 63 +# Offense count: 69 # This cop supports safe autocorrection (--autocorrect). Style/RedundantBegin: Enabled: false -# Offense count: 13 +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods. +# AllowedMethods: nonzero? Style/RedundantCondition: Exclude: - 'app/api/v01/helper/deliverable_by_vehicles_helper.rb' @@ -1517,7 +1782,6 @@ Style/RedundantCondition: - 'app/models/user.rb' - 'app/views/destinations/import_template.csv.ruby' - 'app/views/destinations/import_template.excel.ruby' - - 'app/views/routes/_show.csv.ruby' - 'app/views/stores/import_template.csv.ruby' - 'app/views/stores/import_template.excel.ruby' - 'app/views/vehicle_usage_sets/import_template.csv.ruby' @@ -1536,6 +1800,26 @@ Style/RedundantConditional: - 'app/api/v100/entities/route.rb' - 'app/api/v100/entities/route_properties.rb' +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantConstantBase: + Exclude: + - 'config/environments/production.rb' + - 'docker/production.rb' + - 'spec/rails_helper.rb' + - 'test/test_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantCurrentDirectoryInPath: + Exclude: + - 'lib/devices/fleet.rb' + +# Offense count: 42 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantDoubleSplatHashBraces: + Enabled: false + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/RedundantException: @@ -1556,37 +1840,27 @@ Style/RedundantFileExtensionInRequire: - 'lib/devices/fleet.rb' - 'test/lib/concave_hull_test.rb' -# Offense count: 23 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/RedundantFilterChain: + Exclude: + - 'app/jobs/optimizer_job.rb' + +# Offense count: 29 # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Enabled: false -# Offense count: 17 +# Offense count: 36 # This cop supports safe autocorrection (--autocorrect). Style/RedundantParentheses: - Exclude: - - 'app/api/v01/helper/deliverable_by_vehicles_helper.rb' - - 'app/controllers/application_controller.rb' - - 'app/helpers/vehicles_helper.rb' - - 'app/jobs/importer_destinations.rb' - - 'app/models/route.rb' - - 'app/models/user.rb' - - 'config/initializers/grap.rb' - - 'db/migrate/20180219090520_change_hstore_to_jsonb.rb' - - 'lib/devices/tomtom.rb' - - 'lib/devices/trimble.rb' - - 'lib/exceptions.rb' - - 'test/api/v01/routes_test.rb' - - 'test/models/zoning_test.rb' + Enabled: false -# Offense count: 9 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: +Style/RedundantRegexpArgument: Exclude: - 'app/jobs/importer_destinations.rb' - - 'app/models/concerns/ref_sanitizer.rb' - - 'app/views/routes/_show.csv.ruby' - - 'lib/devices/alyacom.rb' # Offense count: 17 # This cop supports safe autocorrection (--autocorrect). @@ -1609,12 +1883,30 @@ Style/RedundantReturn: - 'lib/devices/stg_telematics.rb' - 'lib/devices/teksat.rb' +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/RedundantSelfAssignment: + Exclude: + - 'app/models/router.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantSelfAssignmentBranch: + Exclude: + - 'app/helpers/vehicle_usages_helper.rb' + # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantSort: Exclude: - 'lib/devices/fleet_demo.rb' +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantStringEscape: + Exclude: + - 'app/services/messagings/messaging_service.rb' + # Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowInnerSlashes. @@ -1655,27 +1947,32 @@ Style/RescueStandardError: - 'test/models/customer_test.rb' - 'test/models/store_test.rb' -# Offense count: 123 +# Offense count: 121 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Enabled: false +# Offense count: 3 +# Configuration parameters: Max. +Style/SafeNavigationChainLength: + Exclude: + - 'test/api/v01/helper/plannings_helper_api_test.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/SelfAssignment: Exclude: - 'app/helpers/application_helper.rb' -# Offense count: 6 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowAsExpressionSeparator. Style/Semicolon: Exclude: - 'app/api/v01/destinations.rb' - 'app/models/planning.rb' - - 'app/models/visit.rb' - 'test/api/v01/devices/tomtom_test.rb' - 'test/jobs/importer_destinations_test.rb' @@ -1688,6 +1985,13 @@ Style/SingleArgumentDig: - 'app/views/destinations/_show.json.jbuilder' - 'lib/optim/optimizer_wrapper.rb' +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +Style/SingleLineDoEndBlock: + Exclude: + - 'app/api/v01/helper/shared_params.rb' + - 'app/api/v01/zonings.rb' + # Offense count: 17 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: @@ -1705,7 +2009,7 @@ Style/SlicingWithRange: - 'test/lib/concave_hull_test.rb' - 'test/models/customer_test.rb' -# Offense count: 12 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. Style/SoleNestedConditional: @@ -1719,7 +2023,6 @@ Style/SoleNestedConditional: - 'app/models/vehicle.rb' - 'app/views/routes/_edit.json.jbuilder' - 'app/views/stops/_show.json.jbuilder' - - 'lib/notifications.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1743,13 +2046,19 @@ Style/StderrPuts: - 'bin/yarn' - 'public/api/0.1/examples/ruby/example.rb' -# Offense count: 367 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/StringChars: + Exclude: + - 'app/helpers/routes_helper.rb' + +# Offense count: 381 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: Enabled: false -# Offense count: 546 +# Offense count: 668 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes @@ -1762,20 +2071,46 @@ Style/StructInheritance: Exclude: - 'test/models/planning_test.rb' +# Offense count: 15 +# This cop supports safe autocorrection (--autocorrect). +Style/SuperArguments: + Exclude: + - 'app/api/v01/api.rb' + - 'app/api/v100/api.rb' + - 'app/controllers/application_controller.rb' + - 'app/helpers/application_helper.rb' + - 'app/job/importer_vehicle_stores.rb' + - 'app/models/concerns/quantity_attr.rb' + - 'app/models/route.rb' + - 'app/services/devices/deliver_service.rb' + - 'app/services/devices/fleet_service.rb' + - 'app/services/devices/stg_telematics_service.rb' + - 'app/services/devices/teksat_service.rb' + - 'app/services/messagings/sms_partner_service.rb' + - 'app/services/messagings/vonage_service.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/SuperWithArgsParentheses: + Exclude: + - 'app/helpers/application_helper.rb' + - 'app/jobs/importer_destinations.rb' + - 'app/services/devices/teksat_service.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/SymbolLiteral: Exclude: - 'lib/devices/masternaut.rb' -# Offense count: 117 +# Offense count: 148 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. # AllowedMethods: define_method Style/SymbolProc: Enabled: false -# Offense count: 19 +# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowSafeAssignment. # SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex @@ -1807,7 +2142,7 @@ Style/WhileUntilDo: - 'lib/concave_hull.rb' - 'lib/routers/here.rb' -# Offense count: 36 +# Offense count: 44 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MinSize, WordRegex. # SupportedStyles: percent, brackets @@ -1823,7 +2158,7 @@ Style/YodaCondition: - 'test/api/v01/customers_test.rb' - 'test/api/v01/geocoder_test.rb' -# Offense count: 58 +# Offense count: 55 # This cop supports unsafe autocorrection (--autocorrect-all). Style/ZeroLengthPredicate: Enabled: false diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb index 063145436..8f569dd98 100644 --- a/app/controllers/images_controller.rb +++ b/app/controllers/images_controller.rb @@ -53,7 +53,7 @@ def point_large private def range(x) - x < 0 ? 0 : x > 1 ? 1 : x + x.clamp(0, 1) end def pal(hex) diff --git a/app/models/route.rb b/app/models/route.rb index fa46a6814..a5bb02980 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -32,7 +32,7 @@ class Route < ApplicationRecord nilify_blanks validates :planning, presence: true -# validates :vehicle_usage, presence: true # nil on unplanned route + # validates :vehicle_usage, presence: true # nil on unplanned route validate :stop_index_validation attr_accessor :no_stop_index_validation, :vehicle_color_changed diff --git a/config/routes.rb b/config/routes.rb index f71c35469..44869e983 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,7 +72,7 @@ end # get 'zonings/:zoning_id/edit' => 'zonings_api_web#edit', :as => 'edit_zoning' - # patch 'zonings/:zoning_id/edit' => 'zonings_api_web#update' + # patch 'zonings/:zoning_id/edit' => 'zonings_api_web#update' resources :tags delete 'tags' => 'tags#destroy_multiple' diff --git a/test/api/v01/plannings_test.rb b/test/api/v01/plannings_test.rb index a2871c7f5..02f6c7725 100644 --- a/test/api/v01/plannings_test.rb +++ b/test/api/v01/plannings_test.rb @@ -186,15 +186,15 @@ def around end end -# test 'should set stop status' do -# patch api("#{@planning.id}/update_stop") -# assert last_response.ok?, last_response.body -# end - -# test 'should starts asynchronous route optimization' do -# get api("#{@planning.id}/optimize_route") -# assert last_response.ok?, last_response.body -# end + # test 'should set stop status' do + # patch api("#{@planning.id}/update_stop") + # assert last_response.ok?, last_response.body + # end + + # test 'should starts asynchronous route optimization' do + # get api("#{@planning.id}/optimize_route") + # assert last_response.ok?, last_response.body + # end test 'should change stops activation' do [:during_optimization, nil].each do |mode| diff --git a/test/api/v01/vehicle_usages_test.rb b/test/api/v01/vehicle_usages_test.rb index ee2e098f2..06c0f9dd4 100644 --- a/test/api/v01/vehicle_usages_test.rb +++ b/test/api/v01/vehicle_usages_test.rb @@ -63,7 +63,7 @@ def api(vehicle_usage_set_id, part = nil, param = {}) tags_str = tags(:tag_one).id.to_s + ',' + tags(:tag_two).id.to_s - #tag_ids can be string coma separated or array of integer + #tag_ids can be string coma separated or array of integer [ [tags(:tag_one).id, tags(:tag_two).id], tags_str diff --git a/test/controllers/plannings_controller_test.rb b/test/controllers/plannings_controller_test.rb index be84f9832..55292ecb8 100644 --- a/test/controllers/plannings_controller_test.rb +++ b/test/controllers/plannings_controller_test.rb @@ -22,7 +22,7 @@ def around Routers::RouterWrapper.stub_any_instance(:matrix, lambda{ |url, mode, dimensions, row, column, options| [Array.new(row.size) { Array.new(column.size, 0) }] }) do # return all services in reverse order in first route, rests at the end OptimizerWrapper.stub_any_instance(:optimize, lambda { |planning, routes, options| - # Put all the stops on the first available route with a vehicle + # Put all the stops on the first available route with a vehicle returned_stops = routes.flat_map{ |r| r.stops.select{ |stop| stop.is_a?(StopVisit) }} first_route = routes.find{ |r| r.vehicle_usage? } first_route_rests = first_route.stops.select{ |stop| stop.is_a?(StopRest) }.compact @@ -805,7 +805,7 @@ def around end test 'should not optimize one route on unprocessable entity' do - # without_loading Stop, if: -> (obj) { obj.route_id != routes(:route_one_one).id } do + # without_loading Stop, if: -> (obj) { obj.route_id != routes(:route_one_one).id } do Customer.stub_any_instance(:save!, lambda { |*a| false } ) do get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } assert_valid response diff --git a/test/lib/concave_hull_test.rb b/test/lib/concave_hull_test.rb index 1b2a59036..38592c5b6 100644 --- a/test/lib/concave_hull_test.rb +++ b/test/lib/concave_hull_test.rb @@ -33,7 +33,7 @@ class ConcaveHullTest < ActionController::TestCase " -# puts svg + # puts svg end test 'should compute specific hull with two points having same x' do diff --git a/test/lib/routers/here_test.rb b/test/lib/routers/here_test.rb index aa9e055d1..461cf722a 100644 --- a/test/lib/routers/here_test.rb +++ b/test/lib/routers/here_test.rb @@ -49,16 +49,16 @@ class Routers::HereTest < ActionController::TestCase end end -# test 'should compute large matrix' do -# SIZE = 100 -# prng = Random.new -# vector = SIZE.times.collect{ [prng.rand(48.811159..48.911218), prng.rand(2.270393..2.435532)] } # Some points in Paris -# #start = Time.now -# matrix = @here.matrix(vector, vector) -# #finish = Time.now -# #puts finish - start -# -# assert_equal SIZE, matrix.size -# assert_equal SIZE, matrix[0].size -# end + # test 'should compute large matrix' do + # SIZE = 100 + # prng = Random.new + # vector = SIZE.times.collect{ [prng.rand(48.811159..48.911218), prng.rand(2.270393..2.435532)] } # Some points in Paris + # #start = Time.now + # matrix = @here.matrix(vector, vector) + # #finish = Time.now + # #puts finish - start + # + # assert_equal SIZE, matrix.size + # assert_equal SIZE, matrix[0].size + # end end diff --git a/test/lib/routers/osrm_test.rb b/test/lib/routers/osrm_test.rb index 3f8a3970e..cf0ca786a 100644 --- a/test/lib/routers/osrm_test.rb +++ b/test/lib/routers/osrm_test.rb @@ -54,16 +54,16 @@ class Routers::OsrmTest < ActionController::TestCase end end -# test 'should compute large matrix' do -# SIZE = 110 -# prng = Random.new -# vector = SIZE.times.collect{ [prng.rand(48.811159..48.911218), prng.rand(2.270393..2.435532)] } # Some points in Paris -# #start = Time.now -# matrix = @osrm.matrix(routers(:router_one).url_time, vector) -# #finish = Time.now -# #puts finish - start -# -# assert_equal SIZE, matrix.size -# assert_equal SIZE, matrix[0].size -# end + # test 'should compute large matrix' do + # SIZE = 110 + # prng = Random.new + # vector = SIZE.times.collect{ [prng.rand(48.811159..48.911218), prng.rand(2.270393..2.435532)] } # Some points in Paris + # #start = Time.now + # matrix = @osrm.matrix(routers(:router_one).url_time, vector) + # #finish = Time.now + # #puts finish - start + # + # assert_equal SIZE, matrix.size + # assert_equal SIZE, matrix[0].size + # end end diff --git a/test/models/customer_test.rb b/test/models/customer_test.rb index c6909d256..22a1652d2 100644 --- a/test/models/customer_test.rb +++ b/test/models/customer_test.rb @@ -362,8 +362,8 @@ def around end test 'should clear all destinations and outdate routes' do - # TODO: activate code when without_loading can be called inside another without_loading with options - # without_loading Stop, if: -> (stop) { o = !stop.is_a?(StopRest); } do + # TODO: activate code when without_loading can be called inside another without_loading with options + # without_loading Stop, if: -> (stop) { o = !stop.is_a?(StopRest); } do without_loading Visit do assert_difference('Stop.count', -6) do assert_difference('Visit.count', -4) do diff --git a/test/models/vehicle_usage_test.rb b/test/models/vehicle_usage_test.rb index 119cc9dc3..c081c13dc 100644 --- a/test/models/vehicle_usage_test.rb +++ b/test/models/vehicle_usage_test.rb @@ -2,10 +2,10 @@ class VehicleUsageTest < ActiveSupport::TestCase -# test 'should not save' do -# o = vehicle_usage_sets(:vehicle_usage_set_one).vehicle_usages.build() -# assert_not o.save, 'Saved without required fields' -# end + # test 'should not save' do + # o = vehicle_usage_sets(:vehicle_usage_set_one).vehicle_usages.build() + # assert_not o.save, 'Saved without required fields' + # end test 'should save' do vehicle_usage = vehicle_usage_sets(:vehicle_usage_set_one).vehicle_usages.build(vehicle: vehicles(:vehicle_one)) diff --git a/test/models/zoning_test.rb b/test/models/zoning_test.rb index 680dd83df..f59cac290 100644 --- a/test/models/zoning_test.rb +++ b/test/models/zoning_test.rb @@ -74,8 +74,8 @@ class ZoningTest < ActiveSupport::TestCase test 'should generate isochrones' do begin - # TODO: An undefined test changes time zone... - # .with(:body => hash_including(size: '5', mode: 'car', traffic: 'true', weight: '10', departure: Date.today.strftime('%Y-%m-%d') + ' 10:00:00 UTC')) + # TODO: An undefined test changes time zone... + # .with(:body => hash_including(size: '5', mode: 'car', traffic: 'true', weight: '10', departure: Date.today.strftime('%Y-%m-%d') + ' 10:00:00 UTC')) stub_isochrone = stub_request(:post, 'http://localhost:5000/0.1/isoline.json') .with(:body => hash_including(size: '5', mode: 'car', traffic: 'true', weight: '10', departure: Date.today.strftime('%Y-%m-%d') + ' 10:00:00 ' + (Time.zone.now.strftime('%z') == '+0000' ? 'UTC' : (Time.zone.now.strftime('%z'))))) .to_return(status: 200, body: File.new(File.expand_path('../../web_mocks/', __FILE__) + '/isochrone/isochrone-1.json').read) diff --git a/test/test_helper.rb b/test/test_helper.rb index f393b8363..2501f4bf1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -90,9 +90,9 @@ def teardown end def assert_valid(response) -# html_validation = PageValidations::HTMLValidation.new -# validation = html_validation.validation(response.body, response.to_s) -# assert validation.valid?, validation.exceptions + # html_validation = PageValidations::HTMLValidation.new + # validation = html_validation.validation(response.body, response.to_s) + # assert validation.valid?, validation.exceptions end def without_loading(klass, options = {}) @@ -163,7 +163,7 @@ def get_jobs(part, param = {}) require 'webdrivers/chromedriver' BENCHMARK_CPU_RATE = cpu_rate 17000 -# Browser testing configuration + # Browser testing configuration class ActionDispatch::IntegrationTest self.use_transactional_fixtures = false From cb5194e7fc1a0ed2f038b51659512feb69464ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 15:57:42 +0200 Subject: [PATCH 15/24] Standardize indentation --- .rubocop.yml | 3 -- app/api/v01/destinations.rb | 36 ++++++------- app/api/v01/devices/fleet_reporting.rb | 4 +- app/api/v01/jobs.rb | 2 +- app/api/v01/routes_get.rb | 12 ++--- app/api/v01/stores.rb | 2 +- app/api/v01/vehicle_usage_sets.rb | 2 +- app/api/v01/vehicle_usages.rb | 2 +- app/api/v01/vehicles.rb | 6 +-- app/api/v01/visits.rb | 6 +-- app/api/v01/visits_get.rb | 6 +-- app/api/v01/zonings.rb | 10 ++-- app/api/v100/destinations.rb | 8 +-- app/api/v100/routes.rb | 8 +-- .../api_web/v01/api_web_controller.rb | 2 +- .../api_web/v01/destinations_controller.rb | 4 +- .../api_web/v01/routes_controller.rb | 13 ++--- .../api_web/v01/stores_controller.rb | 4 +- .../api_web/v01/zones_controller.rb | 6 +-- app/controllers/destinations_controller.rb | 2 +- app/controllers/plannings_controller.rb | 23 ++++---- app/helpers/routes_helper.rb | 2 +- app/jobs/clustering.rb | 2 +- app/jobs/importer_destinations.rb | 6 +-- app/jobs/importer_vehicle_usage_sets.rb | 2 +- app/models/planning.rb | 8 +-- app/models/vehicle.rb | 2 +- app/uploaders/admin/favicon_uploader.rb | 2 +- app/uploaders/admin/logo_large_uploader.rb | 2 +- app/uploaders/admin/logo_small_uploader.rb | 2 +- .../api_web/v01/zones/index.json.jbuilder | 2 +- app/views/routes/_index.csv.ruby | 2 +- app/views/routes/_index.excel.ruby | 2 +- app/views/routes/_show.csv.ruby | 8 +-- app/views/vehicle_usage_sets/show.csv.ruby | 10 ++-- app/views/vehicle_usage_sets/show.excel.ruby | 10 ++-- ...0039_create_rails_admin_histories_table.rb | 24 ++++----- ...1208155944_change_user_unit_column_name.rb | 2 +- ...20161220100839_add_missing_foreign_keys.rb | 14 ++--- lib/devices/fleet.rb | 4 +- lib/devices/tomtom.rb | 28 +++++----- lib/devices/trimble.rb | 54 +++++++++---------- lib/exceptions.rb | 8 +-- lib/optim/optimizer_wrapper.rb | 4 +- spec/features/planning_spec.rb | 2 +- test/api/v01/plannings_test.rb | 2 +- test/api/v100/plannings_routes_test.rb | 16 +++--- test/api/v100/stops_test.rb | 16 +++--- .../v01/destinations_controller_test.rb | 8 +-- .../application_controller_test.rb | 8 +-- test/controllers/plannings_controller_test.rb | 32 +++++------ test/models/customer_test.rb | 14 ++--- 52 files changed, 229 insertions(+), 230 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index ea6eab060..e7c364ab0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -72,9 +72,6 @@ Style/ClassAndModuleChildren: Metrics/ClassLength: Enabled: false -Layout/IndentationWidth: - Enabled: false - Layout/EndAlignment: Enabled: false diff --git a/app/api/v01/destinations.rb b/app/api/v01/destinations.rb index a091a1fd3..6c1a071ae 100644 --- a/app/api/v01/destinations.rb +++ b/app/api/v01/destinations.rb @@ -67,7 +67,7 @@ def destination_params def present_geojson_destinations(params) destinations = if params.key?(:ids) - ids = params[:ids].split(',') + ids = params[:ids].split(',') current_customer.destinations.includes_visits.select{ |destination| params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } } @@ -75,19 +75,19 @@ def present_geojson_destinations(params) current_customer.destinations.includes_visits end '{"type":"FeatureCollection","features":[' + destinations.select(&:position?).map { |d| - feat = { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [d.lng.round(6), d.lat.round(6)] - }, - properties: { - destination_id: d.id, - color: d.visits_color, - icon: d.visits_icon, - icon_size: d.visits_icon_size - } + feat = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [d.lng.round(6), d.lat.round(6)] + }, + properties: { + destination_id: d.id, + color: d.visits_color, + icon: d.visits_icon, + icon_size: d.visits_icon_size } + } feat[:properties][:quantities] = d.visits.map { |v| with_quantities(v) }.flatten if params[:quantities] @@ -138,9 +138,9 @@ def with_quantities(visit) present_geojson_destinations params else destinations = if params.key?(:ids) - current_customer.destinations.includes_visits.select{ |destination| - params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } - } + current_customer.destinations.includes_visits.select{ |destination| + params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } + } else current_customer.destinations.includes_visits.load end @@ -229,8 +229,8 @@ def with_quantities(visit) params[:planning].delete(:zoning_ids) end import = if params[:destinations] - # FIXME ImportJSON has its own conversion methods. It should be done at the API level - ImportJson.new(importer: ImporterDestinations.new(current_customer, params[:planning]), replace: params[:replace], json: import_destination_params) + # FIXME ImportJSON has its own conversion methods. It should be done at the API level + ImportJson.new(importer: ImporterDestinations.new(current_customer, params[:planning]), replace: params[:replace], json: import_destination_params) elsif params[:remote] case params[:remote] when :tomtom then ImportTomtom.new(importer: ImporterDestinations.new(current_customer, params[:planning]), customer: current_customer, replace: params[:replace]) diff --git a/app/api/v01/devices/fleet_reporting.rb b/app/api/v01/devices/fleet_reporting.rb index 710d3588d..832bd6a18 100644 --- a/app/api/v01/devices/fleet_reporting.rb +++ b/app/api/v01/devices/fleet_reporting.rb @@ -49,9 +49,9 @@ def service end get do if params[:end_date] < params[:begin_date] - error!(I18n.t('reporting.download.end_date_inferior'), 400) + error!(I18n.t('reporting.download.end_date_inferior'), 400) elsif (params[:end_date] - params[:begin_date]).to_i > SharedParams::MAX_DAYS - error!(I18n.t('reporting.download.max_interval_reached'), 400) + error!(I18n.t('reporting.download.max_interval_reached'), 400) else service.reporting(params) || status(204) end diff --git a/app/api/v01/jobs.rb b/app/api/v01/jobs.rb index ce6417cb6..a4fa6615f 100644 --- a/app/api/v01/jobs.rb +++ b/app/api/v01/jobs.rb @@ -60,7 +60,7 @@ def job_params get ':id' do customer = current_customer job = if customer.job_optimizer && customer.job_optimizer_id == params[:id] - customer.job_optimizer + customer.job_optimizer elsif customer.job_destination_geocoding && customer.job_destination_geocoding_id == params[:id] customer.job_destination_geocoding elsif customer.job_store_geocoding && customer.job_store_geocoding_id == params[:id] diff --git a/app/api/v01/routes_get.rb b/app/api/v01/routes_get.rb index b5e4049cb..e057059b4 100644 --- a/app/api/v01/routes_get.rb +++ b/app/api/v01/routes_get.rb @@ -58,9 +58,9 @@ def present_routes(routes, params) end get do routes = if params.key?(:ids) - current_customer.plannings.flat_map(&:routes).select{ |route| - params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } - } + current_customer.plannings.flat_map(&:routes).select{ |route| + params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } + } else current_customer.plannings.flat_map{ |p| p.routes.includes_vehicle_usages.load } end @@ -88,9 +88,9 @@ def present_routes(routes, params) get do planning_id = ParseIdsRefs.read(params[:planning_id]) routes = if params.key?(:ids) - current_customer.plannings.where(planning_id).first!.routes.select{ |route| - params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } - } + current_customer.plannings.where(planning_id).first!.routes.select{ |route| + params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } + } else current_customer.plannings.where(planning_id).first!.routes.includes_vehicle_usages.load end diff --git a/app/api/v01/stores.rb b/app/api/v01/stores.rb index a99a7dc57..e3a1f8246 100644 --- a/app/api/v01/stores.rb +++ b/app/api/v01/stores.rb @@ -231,7 +231,7 @@ def store_params put :import_vehicle_stores do import = if params[:stores] - ImportJson.new(importer: ImporterVehicleStores.new(current_customer), replace: params[:replace], json: params[:stores]) + ImportJson.new(importer: ImporterVehicleStores.new(current_customer), replace: params[:replace], json: params[:stores]) else ImportCsv.new(importer: ImporterVehicleStores.new(current_customer), replace: params[:replace], file: params[:file]) end diff --git a/app/api/v01/vehicle_usage_sets.rb b/app/api/v01/vehicle_usage_sets.rb index 3daf2c88c..36abe27b3 100644 --- a/app/api/v01/vehicle_usage_sets.rb +++ b/app/api/v01/vehicle_usage_sets.rb @@ -42,7 +42,7 @@ def vehicle_usage_set_params end get do vehicle_usage_sets = if params.key?(:ids) - current_customer.vehicle_usage_sets.select{ |vehicle_usage_set| params[:ids].include?(vehicle_usage_set.id) } + current_customer.vehicle_usage_sets.select{ |vehicle_usage_set| params[:ids].include?(vehicle_usage_set.id) } else current_customer.vehicle_usage_sets.load end diff --git a/app/api/v01/vehicle_usages.rb b/app/api/v01/vehicle_usages.rb index b92caceb4..43419f246 100644 --- a/app/api/v01/vehicle_usages.rb +++ b/app/api/v01/vehicle_usages.rb @@ -47,7 +47,7 @@ def vehicle_usage_params get do vehicle_usage_set = current_customer.vehicle_usage_sets.where(id: params[:vehicle_usage_set_id]).first vehicle_usages = if vehicle_usage_set && params.key?(:ids) - vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| params[:ids].include?(vehicle_usage.id) } + vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| params[:ids].include?(vehicle_usage.id) } else vehicle_usage_set.vehicle_usages.load end diff --git a/app/api/v01/vehicles.rb b/app/api/v01/vehicles.rb index edcf98611..55ce60a86 100644 --- a/app/api/v01/vehicles.rb +++ b/app/api/v01/vehicles.rb @@ -96,9 +96,9 @@ def deliverables_by_vehicle_params end get do vehicles = if params.key?(:ids) - current_customer.vehicles.select{ |vehicle| - params[:ids].any?{ |s| ParseIdsRefs.match(s, vehicle) } - } + current_customer.vehicles.select{ |vehicle| + params[:ids].any?{ |s| ParseIdsRefs.match(s, vehicle) } + } else current_customer.vehicles.load end diff --git a/app/api/v01/visits.rb b/app/api/v01/visits.rb index 5d6cdc459..49d5302ba 100644 --- a/app/api/v01/visits.rb +++ b/app/api/v01/visits.rb @@ -73,9 +73,9 @@ def visit_params get do destination_id = ParseIdsRefs.read(params[:destination_id]) visits = if params.key?(:ids) - current_customer.destinations.includes_visits.where(destination_id).first!.visits.select{ |visit| - params[:ids].any?{ |s| ParseIdsRefs.match(s, visit) } - } + current_customer.destinations.includes_visits.where(destination_id).first!.visits.select{ |visit| + params[:ids].any?{ |s| ParseIdsRefs.match(s, visit) } + } else current_customer.destinations.includes_visits.where(destination_id).first!.visits.load end diff --git a/app/api/v01/visits_get.rb b/app/api/v01/visits_get.rb index 7f5aab420..acce26706 100644 --- a/app/api/v01/visits_get.rb +++ b/app/api/v01/visits_get.rb @@ -38,9 +38,9 @@ class V01::VisitsGet < Grape::API end get do visits = if params.key?(:ids) - current_customer.visits.select{ |visit| - params[:ids].any?{ |s| ParseIdsRefs.match(s, visit) } - } + current_customer.visits.select{ |visit| + params[:ids].any?{ |s| ParseIdsRefs.match(s, visit) } + } else current_customer.visits end diff --git a/app/api/v01/zonings.rb b/app/api/v01/zonings.rb index ae3b0754a..f1ba63eb2 100644 --- a/app/api/v01/zonings.rb +++ b/app/api/v01/zonings.rb @@ -47,7 +47,7 @@ def zoning_params(d_params = nil) end get do zonings = if params.key?(:ids) - current_customer.zonings.select{ |zoning| params[:ids].include?(zoning.id) } + current_customer.zonings.select{ |zoning| params[:ids].include?(zoning.id) } else current_customer.zonings.load end @@ -184,8 +184,8 @@ def zoning_params(d_params = nil) Zoning.transaction do zoning = current_customer.zonings.where(id: params[:id]).first! vehicle_usage_set = if params.key?(:vehicle_usage_set_id) - vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) - current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } + vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) + current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } else current_customer.vehicle_usage_sets[0] end @@ -247,8 +247,8 @@ def zoning_params(d_params = nil) Zoning.transaction do zoning = current_customer.zonings.where(id: params[:id]).first! vehicle_usage_set = if params.key?(:vehicle_usage_set_id) - vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) - current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } + vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) + current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } else current_customer.vehicle_usage_sets[0] end diff --git a/app/api/v100/destinations.rb b/app/api/v100/destinations.rb index 154338245..2dd5d834b 100644 --- a/app/api/v100/destinations.rb +++ b/app/api/v100/destinations.rb @@ -69,15 +69,15 @@ class V100::Destinations < Grape::API present_geojson_destinations params else destinations = if params[:visits] - current_customer.destinations.includes_visits + current_customer.destinations.includes_visits else current_customer.destinations end destinations = if params.key?(:ids) - destinations.select{ |destination| - params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } - } + destinations.select{ |destination| + params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } + } else destinations.load end diff --git a/app/api/v100/routes.rb b/app/api/v100/routes.rb index e70abd71e..c2ad9e938 100644 --- a/app/api/v100/routes.rb +++ b/app/api/v100/routes.rb @@ -70,10 +70,10 @@ class V100::Routes < Grape::API unless moving_stops.empty? begin - Planning.transaction do - Optimizer.optimize(planning, route, { insertion_only: true, moving_stop_ids: moving_stops.map(&:id) }) - current_customer.save! - end + Planning.transaction do + Optimizer.optimize(planning, route, { insertion_only: true, moving_stop_ids: moving_stops.map(&:id) }) + current_customer.save! + end rescue VRPNoSolutionError error! V100::Status.code_response(:code_304), 304 end diff --git a/app/controllers/api_web/v01/api_web_controller.rb b/app/controllers/api_web/v01/api_web_controller.rb index f3026e7f9..db3c6dd56 100644 --- a/app/controllers/api_web/v01/api_web_controller.rb +++ b/app/controllers/api_web/v01/api_web_controller.rb @@ -25,7 +25,7 @@ class ApiWeb::V01::ApiWebController < ApplicationController private def skip_trackable - request.env['devise.skip_trackable'] = true + request.env['devise.skip_trackable'] = true end def allow_iframe diff --git a/app/controllers/api_web/v01/destinations_controller.rb b/app/controllers/api_web/v01/destinations_controller.rb index 67b9fb968..1745b1976 100644 --- a/app/controllers/api_web/v01/destinations_controller.rb +++ b/app/controllers/api_web/v01/destinations_controller.rb @@ -25,8 +25,8 @@ class ApiWeb::V01::DestinationsController < ApiWeb::V01::ApiWebController def index @customer = current_user.customer @destinations = if params.key?(:ids) - ids = params[:ids].split(',') - current_user.customer.destinations.where(ParseIdsRefs.where(Destination, ids)).includes_visits + ids = params[:ids].split(',') + current_user.customer.destinations.where(ParseIdsRefs.where(Destination, ids)).includes_visits else respond_to do |format| format.html do diff --git a/app/controllers/api_web/v01/routes_controller.rb b/app/controllers/api_web/v01/routes_controller.rb index 420ea713c..fb8024f17 100644 --- a/app/controllers/api_web/v01/routes_controller.rb +++ b/app/controllers/api_web/v01/routes_controller.rb @@ -22,12 +22,13 @@ class ApiWeb::V01::RoutesController < ApiWeb::V01::ApiWebController def index - @routes = if params.key?(:ids) - ids = params[:ids].split(',') - @planning.routes.includes_destinations_and_stores.where(ParseIdsRefs.where(Route, ids)).includes_vehicle_usages - else - @planning.routes.includes_destinations_and_stores.includes_vehicle_usages - end + @routes = + if params.key?(:ids) + ids = params[:ids].split(',') + @planning.routes.includes_destinations_and_stores.where(ParseIdsRefs.where(Route, ids)).includes_vehicle_usages + else + @planning.routes.includes_destinations_and_stores.includes_vehicle_usages + end @layer = current_user.customer.profile.layers.find_by(id: params[:layer_id]) if params[:layer_id] @disable_clusters = ValueToBoolean.value_to_boolean(params[:disable_clusters], false) end diff --git a/app/controllers/api_web/v01/stores_controller.rb b/app/controllers/api_web/v01/stores_controller.rb index a1b0048f2..714100983 100644 --- a/app/controllers/api_web/v01/stores_controller.rb +++ b/app/controllers/api_web/v01/stores_controller.rb @@ -23,8 +23,8 @@ class ApiWeb::V01::StoresController < ApiWeb::V01::ApiWebController def index @customer = current_user.customer @stores = if params.key?(:ids) - ids = params[:ids].split(',') - current_user.customer.stores.where(ParseIdsRefs.where(Store, ids)) + ids = params[:ids].split(',') + current_user.customer.stores.where(ParseIdsRefs.where(Store, ids)) else respond_to do |format| format.html do diff --git a/app/controllers/api_web/v01/zones_controller.rb b/app/controllers/api_web/v01/zones_controller.rb index 5631767ac..7c9bc24a9 100644 --- a/app/controllers/api_web/v01/zones_controller.rb +++ b/app/controllers/api_web/v01/zones_controller.rb @@ -25,8 +25,8 @@ class ApiWeb::V01::ZonesController < ApiWeb::V01::ApiWebController def index @customer = current_user.customer @zones = if params.key?(:ids) - ids = params[:ids].split(',') - @zoning.zones.select{ |zone| ids.include?(zone.id.to_s) } + ids = params[:ids].split(',') + @zoning.zones.select{ |zone| ids.include?(zone.id.to_s) } else @zoning.zones end @@ -41,7 +41,7 @@ def index @stores = current_user.customer.stores.where(ParseIdsRefs.where(Store, params[:store_ids].split(','))) end @vehicle_usage_set = if params[:vehicle_usage_set_id] - current_user.customer.vehicle_usage_sets.find(params[:vehicle_usage_set_id]) + current_user.customer.vehicle_usage_sets.find(params[:vehicle_usage_set_id]) elsif params[:planning_id] current_user.customer.plannings.find(params[:planning_id]).vehicle_usage_set elsif current_user.customer.vehicle_usage_sets.size == 1 diff --git a/app/controllers/destinations_controller.rb b/app/controllers/destinations_controller.rb index 492419e75..b70ef61e3 100644 --- a/app/controllers/destinations_controller.rb +++ b/app/controllers/destinations_controller.rb @@ -32,7 +32,7 @@ class DestinationsController < ApplicationController def index @customer = current_user.customer @destinations = if request.format.html? || !@customer.is_editable? - current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes([:tags]) + current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes([:tags]) else current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes_visits end diff --git a/app/controllers/plannings_controller.rb b/app/controllers/plannings_controller.rb index f27d8e56a..c8bd0822d 100644 --- a/app/controllers/plannings_controller.rb +++ b/app/controllers/plannings_controller.rb @@ -58,20 +58,21 @@ def index def show @params = params @planning = current_user.customer.plannings.where(id: params[:id] || params[:planning_id]).preload_routes_without_stops.first! - @routes = if params[:route_ids] - route_ids = params[:route_ids].split(',').map{ |s| Integer(s) } - @with_stops = true - @planning.routes.where(id: route_ids).includes_destinations_and_stores.includes_vehicle_usages - else - stops_count = 0 - if @planning.routes.select{ |route| !route.hidden || !route.locked || route.vehicle_usage_id.nil? }.none?{ |r| (stops_count += r.stops_size) >= 1000 } + @routes = + if params[:route_ids] + route_ids = params[:route_ids].split(',').map{ |s| Integer(s) } @with_stops = true - @planning.routes.available.includes_destinations_and_stores.includes_vehicle_usages + @planning.routes.where(id: route_ids).includes_destinations_and_stores.includes_vehicle_usages else - @with_stops = false - @planning.routes.available.includes_vehicle_usages + stops_count = 0 + if @planning.routes.select{ |route| !route.hidden || !route.locked || route.vehicle_usage_id.nil? }.none?{ |r| (stops_count += r.stops_size) >= 1000 } + @with_stops = true + @planning.routes.available.includes_destinations_and_stores.includes_vehicle_usages + else + @with_stops = false + @planning.routes.available.includes_vehicle_usages + end end - end respond_to do |format| format.html format.json do diff --git a/app/helpers/routes_helper.rb b/app/helpers/routes_helper.rb index 45f3e072f..7f172e88d 100644 --- a/app/helpers/routes_helper.rb +++ b/app/helpers/routes_helper.rb @@ -106,7 +106,7 @@ def route_devices(devices, route) devices_route.each do |key, value| if devices && devices.key?(key) match_device = if value.is_a?(Array) - { items: devices[key].select{ |dv| value.include? dv[:id] } } + { items: devices[key].select{ |dv| value.include? dv[:id] } } else devices[key].find{ |dv| dv[:id] == value } end diff --git a/app/jobs/clustering.rb b/app/jobs/clustering.rb index 6185d3660..6a26276c1 100644 --- a/app/jobs/clustering.rb +++ b/app/jobs/clustering.rb @@ -95,7 +95,7 @@ def self.hull(factory, cluster, own_multi_point, other_multi_points, buffer) factory.point(i[0], i[1]) } concave_hull = if points.size == 1 - points[0] + points[0] elsif points.size == 2 factory.line_string(points) else diff --git a/app/jobs/importer_destinations.rb b/app/jobs/importer_destinations.rb index 971a061e8..20e041139 100644 --- a/app/jobs/importer_destinations.rb +++ b/app/jobs/importer_destinations.rb @@ -848,9 +848,9 @@ def prepare_visit_with_destination_ref(row, line, destination, destination_index if destination ref_planning = row[:planning_ref].blank? ? nil : row[:planning_ref].downcase visit = if row[:ref_visit] || @nil_visit_available[ref_planning][row[:ref]] - # If nil_visit available retrieve the first visit of the destination with a nil ref_visit - @nil_visit_available[ref_planning][row[:ref]] = false - @existing_visits_by_ref[row[:ref]][row[:ref_visit]] + # If nil_visit available retrieve the first visit of the destination with a nil ref_visit + @nil_visit_available[ref_planning][row[:ref]] = false + @existing_visits_by_ref[row[:ref]][row[:ref_visit]] end @destinations_visits_attributes_by_ref[destination.ref] ||= Hash.new visit_attributes.merge!(destination_id: destination.id) diff --git a/app/jobs/importer_vehicle_usage_sets.rb b/app/jobs/importer_vehicle_usage_sets.rb index f5f599ea4..4296fc57a 100644 --- a/app/jobs/importer_vehicle_usage_sets.rb +++ b/app/jobs/importer_vehicle_usage_sets.rb @@ -176,7 +176,7 @@ def import_row(_name, row, _line, options) # For each vehicle, create vehicle and vehicle usage vehicle = if !row[:ref_vehicle].nil? && !row[:ref_vehicle].strip.empty? && @vehicles_by_ref[row[:ref_vehicle].strip] - @vehicles_by_ref[row[:ref_vehicle].strip] + @vehicles_by_ref[row[:ref_vehicle].strip] else @vehicles_without_ref.shift end diff --git a/app/models/planning.rb b/app/models/planning.rb index 4c53cb3b8..c68514282 100644 --- a/app/models/planning.rb +++ b/app/models/planning.rb @@ -767,10 +767,10 @@ def fetch_stops_status else if DeviceBase.is_fleet_hash?(s) attr = if DeviceBase.is_arrival?(s) - { - arrival_eta: s[:eta], - arrival_status: s[:status] - } + { + arrival_eta: s[:eta], + arrival_status: s[:status] + } else { departure_eta: s[:eta], diff --git a/app/models/vehicle.rb b/app/models/vehicle.rb index 02ddf02b7..f033c6a4a 100644 --- a/app/models/vehicle.rb +++ b/app/models/vehicle.rb @@ -149,7 +149,7 @@ def default_router_options default_router.options.each do |key, value| @current_router_options ||= {} @current_router_options[key.to_s] = if router_options[key.to_s].nil? - customer.router_options[key.to_s] + customer.router_options[key.to_s] else router_options[key.to_s] end diff --git a/app/uploaders/admin/favicon_uploader.rb b/app/uploaders/admin/favicon_uploader.rb index 562ec45ad..d987516a6 100644 --- a/app/uploaders/admin/favicon_uploader.rb +++ b/app/uploaders/admin/favicon_uploader.rb @@ -23,6 +23,6 @@ def store_dir end def extension_white_list - %w(ico png) + %w(ico png) end end diff --git a/app/uploaders/admin/logo_large_uploader.rb b/app/uploaders/admin/logo_large_uploader.rb index 7e6732d2b..89b5ff675 100644 --- a/app/uploaders/admin/logo_large_uploader.rb +++ b/app/uploaders/admin/logo_large_uploader.rb @@ -23,6 +23,6 @@ def store_dir end def extension_white_list - %w(svg png) + %w(svg png) end end diff --git a/app/uploaders/admin/logo_small_uploader.rb b/app/uploaders/admin/logo_small_uploader.rb index d8f8ec06e..7d70e019a 100644 --- a/app/uploaders/admin/logo_small_uploader.rb +++ b/app/uploaders/admin/logo_small_uploader.rb @@ -23,6 +23,6 @@ def store_dir end def extension_white_list - %w(svg png) + %w(svg png) end end diff --git a/app/views/api_web/v01/zones/index.json.jbuilder b/app/views/api_web/v01/zones/index.json.jbuilder index c2f48cf3e..9ff67485e 100644 --- a/app/views/api_web/v01/zones/index.json.jbuilder +++ b/app/views/api_web/v01/zones/index.json.jbuilder @@ -3,7 +3,7 @@ if @vehicle_usage_set vehicle_usages = @zones.select(&:vehicle).collect{ |zone| vehicle_vehicle_usages[zone.vehicle] } end stores = if @stores - @stores + @stores elsif vehicle_usages (vehicle_usages.collect(&:default_store_start) + vehicle_usages.collect(&:default_store_stop) + vehicle_usages.collect(&:default_store_rest)).compact.uniq else diff --git a/app/views/routes/_index.csv.ruby b/app/views/routes/_index.csv.ruby index f9b51d186..190db838a 100644 --- a/app/views/routes/_index.csv.ruby +++ b/app/views/routes/_index.csv.ruby @@ -1,5 +1,5 @@ planning.routes.includes_destinations_and_stores.includes_deliverable_units.select{ |route| route.stops_size > 0 }.select{ |route| - route.vehicle_usage_id || !@params.key?(:stops) || @params[:stops].split('|').include?('out-of-route') + route.vehicle_usage_id || !@params.key?(:stops) || @params[:stops].split('|').include?('out-of-route') }.collect { |route| if summary render partial: 'routes/summary', locals: {route: route, csv: csv} diff --git a/app/views/routes/_index.excel.ruby b/app/views/routes/_index.excel.ruby index 2f1b0ce77..1e47e4d6a 100644 --- a/app/views/routes/_index.excel.ruby +++ b/app/views/routes/_index.excel.ruby @@ -1,5 +1,5 @@ planning.routes.includes_destinations_and_stores.includes_deliverable_units.select{ |route| route.stops_size > 0 }.select{ |route| - route.vehicle_usage_id || !@params.key?(:stops) || @params[:stops].split('|').include?('out-of-route') + route.vehicle_usage_id || !@params.key?(:stops) || @params[:stops].split('|').include?('out-of-route') }.collect { |route| if summary render partial: 'routes/summary', formats: [:csv], locals: {route: route, csv: csv} diff --git a/app/views/routes/_show.csv.ruby b/app/views/routes/_show.csv.ruby index 58854e7a6..f28f83565 100644 --- a/app/views/routes/_show.csv.ruby +++ b/app/views/routes/_show.csv.ruby @@ -144,10 +144,10 @@ route.stops.each { |stop| row.merge!(Hash[route.planning.customer.enable_orders ? [[:orders, stop.is_a?(StopVisit) && stop.order && stop.order.products.length > 0 ? stop.order.products.collect(&:code).join('/') : nil]] : route.planning.customer.deliverable_units.flat_map{ |du| - [ - [('pickup' + (du.label ? "[#{du.label}]" : "#{du.id}")).to_sym, stop.is_a?(StopVisit) ? stop.visit.pickups[du.id] : nil], - [('delivery' + (du.label ? "[#{du.label}]" : "#{du.id}")).to_sym, stop.is_a?(StopVisit) ? stop.visit.deliveries[du.id] : nil] - ] + [ + [('pickup' + (du.label ? "[#{du.label}]" : "#{du.id}")).to_sym, stop.is_a?(StopVisit) ? stop.visit.pickups[du.id] : nil], + [('delivery' + (du.label ? "[#{du.label}]" : "#{du.id}")).to_sym, stop.is_a?(StopVisit) ? stop.visit.deliveries[du.id] : nil] + ] } ]) row.merge!( diff --git a/app/views/vehicle_usage_sets/show.csv.ruby b/app/views/vehicle_usage_sets/show.csv.ruby index b7dbe88bb..c409edb53 100644 --- a/app/views/vehicle_usage_sets/show.csv.ruby +++ b/app/views/vehicle_usage_sets/show.csv.ruby @@ -44,12 +44,12 @@ CSV.generate { |csv| device_keys = {} Planner::Application.config.devices.to_h.each { |device_name, device_object| - if device_object.respond_to?('definition') - device_definition = device_object.definition - if device_definition.key?(:forms) && device_definition[:forms].key?(:vehicle) - device_keys[device_name] = device_definition[:forms][:vehicle].keys - end + if device_object.respond_to?('definition') + device_definition = device_object.definition + if device_definition.key?(:forms) && device_definition[:forms].key?(:vehicle) + device_keys[device_name] = device_definition[:forms][:vehicle].keys end + end } @vehicle_usage_set.vehicle_usages.each { |vehicle_usage| diff --git a/app/views/vehicle_usage_sets/show.excel.ruby b/app/views/vehicle_usage_sets/show.excel.ruby index ac473d328..499e11af6 100644 --- a/app/views/vehicle_usage_sets/show.excel.ruby +++ b/app/views/vehicle_usage_sets/show.excel.ruby @@ -44,12 +44,12 @@ CSV.generate(**{col_sep: ';', row_sep: "\r\n"}) { |csv| device_keys = {} Planner::Application.config.devices.to_h.each { |device_name, device_object| - if device_object.respond_to?('definition') - device_definition = device_object.definition - if device_definition.key?(:forms) && device_definition[:forms].key?(:vehicle) - device_keys[device_name] = device_definition[:forms][:vehicle].keys - end + if device_object.respond_to?('definition') + device_definition = device_object.definition + if device_definition.key?(:forms) && device_definition[:forms].key?(:vehicle) + device_keys[device_name] = device_definition[:forms][:vehicle].keys end + end } @vehicle_usage_set.vehicle_usages.each { |vehicle_usage| diff --git a/db/migrate/20130807200039_create_rails_admin_histories_table.rb b/db/migrate/20130807200039_create_rails_admin_histories_table.rb index 3c743aa28..6a11f138d 100644 --- a/db/migrate/20130807200039_create_rails_admin_histories_table.rb +++ b/db/migrate/20130807200039_create_rails_admin_histories_table.rb @@ -1,16 +1,16 @@ class CreateRailsAdminHistoriesTable < ActiveRecord::Migration - def self.up - create_table :rails_admin_histories do |t| - t.text :message # title, name, or object_id - t.string :username - t.integer :item - t.string :table - t.integer :month, :limit => 2 - t.integer :year, :limit => 5 - t.timestamps - end - add_index(:rails_admin_histories, [:item, :table, :month, :year], :name => 'index_rails_admin_histories' ) - end + def self.up + create_table :rails_admin_histories do |t| + t.text :message # title, name, or object_id + t.string :username + t.integer :item + t.string :table + t.integer :month, :limit => 2 + t.integer :year, :limit => 5 + t.timestamps + end + add_index(:rails_admin_histories, [:item, :table, :month, :year], :name => 'index_rails_admin_histories' ) + end def self.down drop_table :rails_admin_histories diff --git a/db/migrate/20161208155944_change_user_unit_column_name.rb b/db/migrate/20161208155944_change_user_unit_column_name.rb index eae4d4fec..b400b1833 100644 --- a/db/migrate/20161208155944_change_user_unit_column_name.rb +++ b/db/migrate/20161208155944_change_user_unit_column_name.rb @@ -1,5 +1,5 @@ class ChangeUserUnitColumnName < ActiveRecord::Migration def change - rename_column :users, :prefered_unity, :prefered_unit + rename_column :users, :prefered_unity, :prefered_unit end end diff --git a/db/migrate/20161220100839_add_missing_foreign_keys.rb b/db/migrate/20161220100839_add_missing_foreign_keys.rb index a9098ed7d..26bc311a6 100644 --- a/db/migrate/20161220100839_add_missing_foreign_keys.rb +++ b/db/migrate/20161220100839_add_missing_foreign_keys.rb @@ -1,15 +1,15 @@ class AddMissingForeignKeys < ActiveRecord::Migration def change - add_foreign_key :customers, :resellers + add_foreign_key :customers, :resellers - add_foreign_key :deliverable_units, :customers + add_foreign_key :deliverable_units, :customers - add_foreign_key :layers_profiles, :profiles - add_foreign_key :layers_profiles, :layers + add_foreign_key :layers_profiles, :profiles + add_foreign_key :layers_profiles, :layers - add_foreign_key :profiles_routers, :profiles - add_foreign_key :profiles_routers, :routers + add_foreign_key :profiles_routers, :profiles + add_foreign_key :profiles_routers, :routers - add_foreign_key :users, :resellers + add_foreign_key :users, :resellers end end diff --git a/lib/devices/fleet.rb b/lib/devices/fleet.rb index ba1892205..aef7a6834 100644 --- a/lib/devices/fleet.rb +++ b/lib/devices/fleet.rb @@ -454,8 +454,8 @@ def generate_store_id(store, route, date, options) def generate_mission_id(stop, date) order_id = if stop.is_a?(StopVisit) - ref = [stop.visit.ref, stop.ref].compact.join('-') - (ref.blank? ? '' : ref + '-') + "v#{stop.visit_id}" + ref = [stop.visit.ref, stop.ref].compact.join('-') + (ref.blank? ? '' : ref + '-') + "v#{stop.visit_id}" else "r#{stop.id}" end diff --git a/lib/devices/tomtom.rb b/lib/devices/tomtom.rb index 5029e4211..e13ba5bda 100644 --- a/lib/devices/tomtom.rb +++ b/lib/devices/tomtom.rb @@ -221,22 +221,22 @@ def send_route(customer, route, options = {}) route.vehicle_usage.default_store_stop.name ]] : [] waypoints = route.stops.select(&:active).collect{ |stop| - position = stop if stop.position? - if position.nil? || position.lat.nil? || position.lng.nil? - next - end - [ - position.lat, - position.lng, - stop.is_a?(StopVisit) ? (customer.enable_orders ? (stop.order ? stop.order.products.collect(&:code).join(',') : '') : customer.deliverable_units.map{ |du| stop.visit.default_quantities[du.id] && "x#{stop.visit.default_quantities[du.id]}#{du.label}" }.compact.join(' ')) : nil, - stop.name, - stop.comment, - stop.phone_number - ] + position = stop if stop.position? + if position.nil? || position.lat.nil? || position.lng.nil? + next + end + [ + position.lat, + position.lng, + stop.is_a?(StopVisit) ? (customer.enable_orders ? (stop.order ? stop.order.products.collect(&:code).join(',') : '') : customer.deliverable_units.map{ |du| stop.visit.default_quantities[du.id] && "x#{stop.visit.default_quantities[du.id]}#{du.label}" }.compact.join(' ')) : nil, + stop.name, + stop.comment, + stop.phone_number + ] } waypoints = (waypoint_start + waypoints.compact + waypoint_stop).map{ |l| - description = l[2..-1].compact.join(' ').strip - {lat: l[0], lng: l[1], description: description} + description = l[2..-1].compact.join(' ').strip + {lat: l[0], lng: l[1], description: description} } position = route.vehicle_usage.default_store_stop if route.vehicle_usage.default_store_stop && route.vehicle_usage.default_store_stop.position? description = route.ref || (waypoints[-1] && waypoints[-1][:description]) || "#{waypoints[-1][:lat]} #{waypoints[-1][:lng]}" diff --git a/lib/devices/trimble.rb b/lib/devices/trimble.rb index 4c438c90e..e6a25e371 100644 --- a/lib/devices/trimble.rb +++ b/lib/devices/trimble.rb @@ -78,36 +78,36 @@ def send_route(customer, route, _options = {}) route.vehicle_usage.default_store_stop.name ]] : [] tasks = route.stops.select(&:active).collect{ |stop| - position = stop if stop.position? - if position.nil? || position.lat.nil? || position.lng.nil? - next - end - [ - position.lat, - position.lng, - stop.is_a?(StopVisit) ? (route.planning.customer.enable_orders ? (stop.order ? stop.order.products.collect(&:code).join(',') : '') : route.planning.customer.deliverable_units.map{ |du| stop.visit.default_quantities[du.id] && "x#{stop.visit.default_quantities[du.id]}#{du.label}" }.compact.join(' ')) : nil, - stop.name, - stop.comment, - stop.phone_number - ] + position = stop if stop.position? + if position.nil? || position.lat.nil? || position.lng.nil? + next + end + [ + position.lat, + position.lng, + stop.is_a?(StopVisit) ? (route.planning.customer.enable_orders ? (stop.order ? stop.order.products.collect(&:code).join(',') : '') : route.planning.customer.deliverable_units.map{ |du| stop.visit.default_quantities[du.id] && "x#{stop.visit.default_quantities[du.id]}#{du.label}" }.compact.join(' ')) : nil, + stop.name, + stop.comment, + stop.phone_number + ] } tasks = (task_start + tasks.compact + task_stop).each_with_index.map{ |v, k| - description = v[2..-1].compact.join(' ').strip - { - id: "r#{route.id}_#{k}", - name: v[3], - description: description, - contact: { - phone: v[5], - coordinate: {latitude: v[0], longitude: v[1]} - }.compact, - activity: { - id: "r#{route.id}_#{k}_0", - # MAPO_1 = Collecte / Chargement - # MAPO_2 = Déchargement - type: 'MAPO_2' - } + description = v[2..-1].compact.join(' ').strip + { + id: "r#{route.id}_#{k}", + name: v[3], + description: description, + contact: { + phone: v[5], + coordinate: {latitude: v[0], longitude: v[1]} + }.compact, + activity: { + id: "r#{route.id}_#{k}_0", + # MAPO_1 = Collecte / Chargement + # MAPO_2 = Déchargement + type: 'MAPO_2' } + } } params = { diff --git a/lib/exceptions.rb b/lib/exceptions.rb index 425657c1b..0c76a2930 100644 --- a/lib/exceptions.rb +++ b/lib/exceptions.rb @@ -108,10 +108,10 @@ def model_record(hash_to_be_modeled) def push_uniq_record(new_record, key) valid = true @nested_hash_error[key].each_with_index do |element, index| - next unless element[:id] == new_record[:id] - valid = false - @nested_hash_error[key][index][:nested_attr] = new_record[:nested_attr] # Update value if needed - break + next unless element[:id] == new_record[:id] + valid = false + @nested_hash_error[key][index][:nested_attr] = new_record[:nested_attr] # Update value if needed + break end @nested_hash_error[key].push(new_record) if valid end diff --git a/lib/optim/optimizer_wrapper.rb b/lib/optim/optimizer_wrapper.rb index fef51851b..dc784a8a8 100644 --- a/lib/optim/optimizer_wrapper.rb +++ b/lib/optim/optimizer_wrapper.rb @@ -169,10 +169,10 @@ def build_configuration(**options) service_ratio = options[:moving_stop_ids]&.any? && options[:service_count].to_i > 0 ? options[:moving_stop_ids].size.to_f / options[:service_count] : 1 service_ratio = [service_ratio, 0.2].max optim_duration_min = if options[:optimize_minimal_time] - (service_ratio * options[:optimize_minimal_time] * options[:vehicle_count] * 1000).to_i + (service_ratio * options[:optimize_minimal_time] * options[:vehicle_count] * 1000).to_i end optim_duration_max = if options[:optimize_time] - (service_ratio * options[:optimize_time] * options[:vehicle_count]).to_i + (service_ratio * options[:optimize_time] * options[:vehicle_count]).to_i end { preprocessing: { diff --git a/spec/features/planning_spec.rb b/spec/features/planning_spec.rb index 248a9a177..75f6c62b1 100644 --- a/spec/features/planning_spec.rb +++ b/spec/features/planning_spec.rb @@ -20,7 +20,7 @@ assert_equal 'P', find_field('planning[name]').value within(:css, '.select2-choices') do - assert_text('tag1') + assert_text('tag1') end first('.route') diff --git a/test/api/v01/plannings_test.rb b/test/api/v01/plannings_test.rb index 02f6c7725..abd1be56c 100644 --- a/test/api/v01/plannings_test.rb +++ b/test/api/v01/plannings_test.rb @@ -32,7 +32,7 @@ def around routes.select{ |r| r.vehicle_usage? }.map.with_index{ |r, i| [r.id, ((i.zero? ? returned_stops.reverse : []) + first_route_rests).map(&:id)] } ).to_h }) do - yield + yield end end end diff --git a/test/api/v100/plannings_routes_test.rb b/test/api/v100/plannings_routes_test.rb index 54df03926..229863416 100644 --- a/test/api/v100/plannings_routes_test.rb +++ b/test/api/v100/plannings_routes_test.rb @@ -15,14 +15,14 @@ def app def around Routers::RouterWrapper.stub_any_instance(:compute_batch, lambda { |url, mode, dimension, segments, options| segments.collect{ |i| [1000, 60, '_ibE_seK_seK_seK'] } } ) do OptimizerWrapper.stub_any_instance(:optimize, lambda { |planning, routes, options| - # Put all the stops on the first available route with a vehicle - returned_stops = routes.flat_map{ |r| r.stops.select{ |stop| stop.is_a?(StopVisit) }} - first_route = routes.find{ |r| r.vehicle_usage? } - first_route_rests = first_route.stops.select{ |stop| stop.is_a?(StopRest) }.compact - ( - routes.select{ |r| !r.vehicle_usage? }.map{ |r| [r.id, []] } + - routes.select{ |r| r.vehicle_usage? }.map.with_index{ |r, i| [r.id, ((i.zero? ? returned_stops.reverse : []) + first_route_rests).map(&:id) + options[:moving_stop_ids]] }.uniq - ).to_h + # Put all the stops on the first available route with a vehicle + returned_stops = routes.flat_map{ |r| r.stops.select{ |stop| stop.is_a?(StopVisit) }} + first_route = routes.find{ |r| r.vehicle_usage? } + first_route_rests = first_route.stops.select{ |stop| stop.is_a?(StopRest) }.compact + ( + routes.select{ |r| !r.vehicle_usage? }.map{ |r| [r.id, []] } + + routes.select{ |r| r.vehicle_usage? }.map.with_index{ |r, i| [r.id, ((i.zero? ? returned_stops.reverse : []) + first_route_rests).map(&:id) + options[:moving_stop_ids]] }.uniq + ).to_h }) do yield end diff --git a/test/api/v100/stops_test.rb b/test/api/v100/stops_test.rb index d2fc0efc7..3f9c26c46 100644 --- a/test/api/v100/stops_test.rb +++ b/test/api/v100/stops_test.rb @@ -18,14 +18,14 @@ def app def around Routers::RouterWrapper.stub_any_instance(:compute_batch, lambda { |url, mode, dimension, segments, options| segments.collect{ |i| [1000, 60, '_ibE_seK_seK_seK'] } } ) do OptimizerWrapper.stub_any_instance(:optimize, lambda { |planning, routes, options| - # Put all the stops on the first available route with a vehicle - returned_stops = routes.flat_map{ |r| r.stops.select{ |stop| stop.is_a?(StopVisit) }} - first_route = routes.find{ |r| r.vehicle_usage? } - first_route_rests = first_route.stops.select{ |stop| stop.is_a?(StopRest) }.compact - ( - routes.select{ |r| !r.vehicle_usage? }.map{ |r| [r.id, []] } + - routes.select{ |r| r.vehicle_usage? }.map.with_index{ |r, i| [r.id, ((i.zero? ? returned_stops.reverse : []) + first_route_rests).map(&:id) + options[:moving_stop_ids]] }.uniq - ).to_h + # Put all the stops on the first available route with a vehicle + returned_stops = routes.flat_map{ |r| r.stops.select{ |stop| stop.is_a?(StopVisit) }} + first_route = routes.find{ |r| r.vehicle_usage? } + first_route_rests = first_route.stops.select{ |stop| stop.is_a?(StopRest) }.compact + ( + routes.select{ |r| !r.vehicle_usage? }.map{ |r| [r.id, []] } + + routes.select{ |r| r.vehicle_usage? }.map.with_index{ |r, i| [r.id, ((i.zero? ? returned_stops.reverse : []) + first_route_rests).map(&:id) + options[:moving_stop_ids]] }.uniq + ).to_h }) do yield end diff --git a/test/controllers/api_web/v01/destinations_controller_test.rb b/test/controllers/api_web/v01/destinations_controller_test.rb index beb5ea34d..60d82a828 100644 --- a/test/controllers/api_web/v01/destinations_controller_test.rb +++ b/test/controllers/api_web/v01/destinations_controller_test.rb @@ -31,10 +31,10 @@ class ApiWeb::V01::DestinationsControllerTest < ActionController::TestCase end test 'should get index in html' do - get :index, params: { format: :html } - assert_response :success - assert_nil assigns(:destinations) - assert_valid response + get :index, params: { format: :html } + assert_response :success + assert_nil assigns(:destinations) + assert_valid response end test 'should get index in json' do diff --git a/test/controllers/application_controller_test.rb b/test/controllers/application_controller_test.rb index e2e4bbf93..f159cd98a 100644 --- a/test/controllers/application_controller_test.rb +++ b/test/controllers/application_controller_test.rb @@ -55,9 +55,9 @@ def index end test 'should rescue server error' do - ApplicationController.stub_any_instance(:api_key?, lambda { |*a| raise ActionController::InvalidAuthenticityToken.new }) do - get :index, params: { format: :json } - assert_response :internal_server_error - end + ApplicationController.stub_any_instance(:api_key?, lambda { |*a| raise ActionController::InvalidAuthenticityToken.new }) do + get :index, params: { format: :json } + assert_response :internal_server_error + end end end diff --git a/test/controllers/plannings_controller_test.rb b/test/controllers/plannings_controller_test.rb index 55292ecb8..3bc8e24a0 100644 --- a/test/controllers/plannings_controller_test.rb +++ b/test/controllers/plannings_controller_test.rb @@ -805,24 +805,24 @@ def around end test 'should not optimize one route on unprocessable entity' do - # without_loading Stop, if: -> (obj) { obj.route_id != routes(:route_one_one).id } do - Customer.stub_any_instance(:save!, lambda { |*a| false } ) do - get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } - assert_valid response - assert_response :unprocessable_entity - end + # without_loading Stop, if: -> (obj) { obj.route_id != routes(:route_one_one).id } do + Customer.stub_any_instance(:save!, lambda { |*a| false } ) do + get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } + assert_valid response + assert_response :unprocessable_entity + end - Planning.stub_any_instance(:optimize, lambda { |*a| raise VRPNoSolutionError } ) do - get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } - assert_valid response - assert_response :unprocessable_entity - end + Planning.stub_any_instance(:optimize, lambda { |*a| raise VRPNoSolutionError } ) do + get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } + assert_valid response + assert_response :unprocessable_entity + end - Planning.stub_any_instance(:optimize, lambda { |*a| raise ActiveRecord::RecordInvalid.new(self) } ) do - get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } - assert_valid response - assert_response :unprocessable_entity - end + Planning.stub_any_instance(:optimize, lambda { |*a| raise ActiveRecord::RecordInvalid.new(self) } ) do + get :optimize_route, params: { planning_id: @planning, format: :js, route_id: routes(:route_one_one).id } + assert_valid response + assert_response :unprocessable_entity + end # end end diff --git a/test/models/customer_test.rb b/test/models/customer_test.rb index 22a1652d2..81af39173 100644 --- a/test/models/customer_test.rb +++ b/test/models/customer_test.rb @@ -362,15 +362,15 @@ def around end test 'should clear all destinations and outdate routes' do - # TODO: activate code when without_loading can be called inside another without_loading with options - # without_loading Stop, if: -> (stop) { o = !stop.is_a?(StopRest); } do - without_loading Visit do - assert_difference('Stop.count', -6) do - assert_difference('Visit.count', -4) do - @customer.delete_all_destinations - end + # TODO: activate code when without_loading can be called inside another without_loading with options + # without_loading Stop, if: -> (stop) { o = !stop.is_a?(StopRest); } do + without_loading Visit do + assert_difference('Stop.count', -6) do + assert_difference('Visit.count', -4) do + @customer.delete_all_destinations end end + end # end assert plannings(:planning_one).routes.all? { |r| r.outdated } end From 75e4435168ccda5cba462f9cdcdbdd10cdaf3708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 16:06:38 +0200 Subject: [PATCH 16/24] Remove trailing commas in arrays --- .rubocop.yml | 9 --------- app/api/api_root.rb | 2 +- app/api/api_v01.rb | 4 ++-- app/api/api_v100.rb | 4 ++-- app/api/v01/jobs.rb | 2 +- app/controllers/plannings_controller.rb | 4 ++-- app/helpers/destinations_helper.rb | 2 +- app/jobs/importer_destinations.rb | 2 +- app/views/order_arrays/_show.csv.ruby | 8 ++++---- config/initializers/browser.rb | 2 +- lib/devices/alyacom.rb | 4 ++-- lib/devices/fleet_modules/fleet_builder.rb | 2 +- lib/devices/masternaut.rb | 2 +- lib/devices/praxedo.rb | 4 ++-- lib/devices/tomtom.rb | 2 +- lib/optim/optimizer_wrapper.rb | 2 +- test/test_helper.rb | 2 +- 17 files changed, 24 insertions(+), 33 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index e7c364ab0..0f28fd916 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,24 +15,15 @@ Metrics/MethodLength: Metrics/AbcSize: Enabled: false -Style/AsciiComments: - Enabled: false - Metrics/PerceivedComplexity: Enabled: false Metrics/CyclomaticComplexity: Enabled: false -Layout/ParameterAlignment: - Enabled: false - Style/DoubleNegation: Enabled: false -Style/TrailingCommaInArrayLiteral: - Enabled: false - Style/TrailingCommaInHashLiteral: Enabled: false diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 3127cf72e..3bfde98b8 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -25,7 +25,7 @@ class ApiRootDef < Grape::API logger.formatter = ENV['LOG_FORMAT'] == 'json' ? GrapeLogging::Formatters::Json.new : GrapeLogging::Formatters::Default.new insert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, logger: logger, include: [ GrapeLogging::Loggers::FilterParameters.new, - GrapeLogging::Loggers::ClientEnv.new, + GrapeLogging::Loggers::ClientEnv.new ] end diff --git a/app/api/api_v01.rb b/app/api/api_v01.rb index 3125dc5ab..2a72802b3 100644 --- a/app/api/api_v01.rb +++ b/app/api/api_v01.rb @@ -39,12 +39,12 @@ class ApiV01 < Grape::API array_use_braces: true, consumes: [ 'application/json; charset=UTF-8', - 'application/xml', + 'application/xml' ], produces: [ 'application/json; charset=UTF-8', 'application/vnd.geo+json; charset=UTF-8', - 'application/xml', + 'application/xml' ], doc_version: nil, security_definitions: { diff --git a/app/api/api_v100.rb b/app/api/api_v100.rb index c3f49a042..f0fe51357 100644 --- a/app/api/api_v100.rb +++ b/app/api/api_v100.rb @@ -22,12 +22,12 @@ class ApiV100 < Grape::API array_use_braces: true, consumes: [ 'application/json; charset=UTF-8', - 'application/xml', + 'application/xml' ], produces: [ 'application/json; charset=UTF-8', 'application/vnd.geo+json; charset=UTF-8', - 'application/xml', + 'application/xml' ], doc_version: '100.0.0', security_definitions: { diff --git a/app/api/v01/jobs.rb b/app/api/v01/jobs.rb index a4fa6615f..4911d43d2 100644 --- a/app/api/v01/jobs.rb +++ b/app/api/v01/jobs.rb @@ -45,7 +45,7 @@ def job_params jobs = [ current_customer.job_optimizer, current_customer.job_destination_geocoding, - current_customer.job_store_geocoding, + current_customer.job_store_geocoding ].compact # .select{ |job| job.failed_at.nil? } present jobs, with: V01::Entities::Job end diff --git a/app/controllers/plannings_controller.rb b/app/controllers/plannings_controller.rb index c8bd0822d..239dd1a0d 100644 --- a/app/controllers/plannings_controller.rb +++ b/app/controllers/plannings_controller.rb @@ -729,7 +729,7 @@ def export_columns :street, :detail, :postalcode, - :city, + :city ] + ((@customer || @planning.customer).with_state? ? [:state] : []) + [ :country, :lat, @@ -787,7 +787,7 @@ def export_summary_columns :cost_fixed, :cost_time, :revenue, - :tags, + :tags ] + ( (@customer || @planning.customer).enable_orders ? [:orders] : diff --git a/app/helpers/destinations_helper.rb b/app/helpers/destinations_helper.rb index 5dae2d9a4..c19e24b33 100644 --- a/app/helpers/destinations_helper.rb +++ b/app/helpers/destinations_helper.rb @@ -67,7 +67,7 @@ def csv_columns_content(destination, customer, options = {}) destination.street, destination.detail, destination.postalcode, - destination.city, + destination.city ] + (customer.with_state? ? [destination.state] : []) + [ destination.country, destination.lat&.round(6), diff --git a/app/jobs/importer_destinations.rb b/app/jobs/importer_destinations.rb index 20e041139..97504a0d1 100644 --- a/app/jobs/importer_destinations.rb +++ b/app/jobs/importer_destinations.rb @@ -93,7 +93,7 @@ def columns_visit [ ["quantity#{du.id}".to_sym, {title: I18n.t('destinations.import_file.quantity') + (du.label ? "[#{du.label}]" : "#{du.id}"), desc: I18n.t('destinations.import_file.quantity_desc'), format: I18n.t('destinations.import_file.format.float')}], ["pickup#{du.id}".to_sym, {title: I18n.t('destinations.import_file.pickup') + (du.label ? "[#{du.label}]" : "#{du.id}"), desc: I18n.t('destinations.import_file.pickup_desc'), format: I18n.t('destinations.import_file.format.float')}], - ["delivery#{du.id}".to_sym, {title: I18n.t('destinations.import_file.delivery') + (du.label ? "[#{du.label}]" : "#{du.id}"), desc: I18n.t('destinations.import_file.delivery_desc'), format: I18n.t('destinations.import_file.format.float')}], + ["delivery#{du.id}".to_sym, {title: I18n.t('destinations.import_file.delivery') + (du.label ? "[#{du.label}]" : "#{du.id}"), desc: I18n.t('destinations.import_file.delivery_desc'), format: I18n.t('destinations.import_file.format.float')}] ] }]).merge(Hash[@customer.custom_attributes.for_visit.map { |ca| ["custom_attributes_visit[#{ca.name}]", { title: "#{I18n.t('destinations.import_file.custom_attributes_visit')}[#{ca.name}]", format: I18n.t("destinations.import_file.format.#{ca.object_type}")}] diff --git a/app/views/order_arrays/_show.csv.ruby b/app/views/order_arrays/_show.csv.ruby index 1bcefbd90..057695596 100644 --- a/app/views/order_arrays/_show.csv.ruby +++ b/app/views/order_arrays/_show.csv.ruby @@ -4,7 +4,7 @@ if params[:planning_id] end header += [ I18n.t('order_arrays.export_file.name'), - I18n.t('order_arrays.export_file.comment'), + I18n.t('order_arrays.export_file.comment') ] + @order_array.days.times.collect { |i| l @order_array.base_date + i } + @order_array.customer.products.collect(&:code) + [ @@ -22,7 +22,7 @@ sum_column = Hash.new { |h,k| h[k] = {} } end line += [ visit_orders[0].visit.destination.name, - visit_orders[0].visit.destination.comment, + visit_orders[0].visit.destination.comment ] + visit_orders.collect { |order| order.products.each { |product| sum_column[order.shift][product] = (sum_column[order.shift][product] || 0) + 1 @@ -44,7 +44,7 @@ shift = 0 @order_array.customer.products.each { |product| csv << [ product.code, - product.name, + product.name ] + @order_array.days.times.collect { |i| total_column[i] = sum_column[i][product] ? (total_column[i] || 0) + sum_column[i][product] : total_column[i] grand_total[product] += sum_column[i][product] || 0 @@ -55,7 +55,7 @@ shift = 0 csv << [ I18n.t('order_arrays.export_file.total'), - nil, + nil ] + @order_array.days.times.collect { |i| total_column[i] } + [nil] * @order_array.customer.products.size + [grand_total.values.sum] diff --git a/config/initializers/browser.rb b/config/initializers/browser.rb index bd70a5964..de7c8e0f0 100644 --- a/config/initializers/browser.rb +++ b/config/initializers/browser.rb @@ -5,7 +5,7 @@ def modern_browser?(browser) browser.firefox?(">= 52"), browser.edge?(">= 16"), browser.opera?(">= 50"), - browser.facebook? && browser.safari_webapp_mode? && browser.webkit_full_version.to_i >= 602, + browser.facebook? && browser.safari_webapp_mode? && browser.webkit_full_version.to_i >= 602 ].any? end diff --git a/lib/devices/alyacom.rb b/lib/devices/alyacom.rb index 5b56dbd42..fc70d0cd1 100644 --- a/lib/devices/alyacom.rb +++ b/lib/devices/alyacom.rb @@ -71,7 +71,7 @@ def send_route(customer, route, _options = {}) stop.time_window_start_1 || stop.time_window_end_1 ? (stop.time_window_start_1 ? stop.time_window_start_1_time + number_of_days(stop.time_window_start_1) : '') + (stop.time_window_start_1 && stop.time_window_end_1 ? '-' : '') + (stop.time_window_end_1 ? (stop.time_window_end_1_time + number_of_days(stop.time_window_end_1) || '') : '') : nil, stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil, stop.comment, - stop.ref, + stop.ref ].compact.join(' ').strip }, planning: { @@ -79,7 +79,7 @@ def send_route(customer, route, _options = {}) staff_id: route.vehicle_usage.vehicle.name, destination_id: stop.base_id, comment: [ - stop.is_a?(StopVisit) ? (customer.enable_orders ? (stop.order ? stop.order.products.collect(&:code).join(',') : '') : customer.deliverable_units.map{ |du| stop.visit.default_quantities[du.id] && "x#{stop.visit.default_quantities[du.id]}#{du.label}" }.compact.join(' ')) : nil, + stop.is_a?(StopVisit) ? (customer.enable_orders ? (stop.order ? stop.order.products.collect(&:code).join(',') : '') : customer.deliverable_units.map{ |du| stop.visit.default_quantities[du.id] && "x#{stop.visit.default_quantities[du.id]}#{du.label}" }.compact.join(' ')) : nil ].compact.join(' ').strip, start: planning_date(route.planning) + stop.time, end: planning_date(route.planning) + stop.time + stop.duration diff --git a/lib/devices/fleet_modules/fleet_builder.rb b/lib/devices/fleet_modules/fleet_builder.rb index 3af4449e5..2d06c645f 100644 --- a/lib/devices/fleet_modules/fleet_builder.rb +++ b/lib/devices/fleet_modules/fleet_builder.rb @@ -68,7 +68,7 @@ def build_route_with_missions(route, customer) lon: stop.lng }, comment: is_visit ? [ - stop.comment, + stop.comment # stop.priority ? I18n.t('activerecord.attributes.visit.priority') + I18n.t('text.separator') + stop.priority_text : nil, # labels.present? ? I18n.t('activerecord.attributes.visit.tags') + I18n.t('text.separator') + labels : nil, ].compact.join("\r\n\r\n").strip : nil, diff --git a/lib/devices/masternaut.rb b/lib/devices/masternaut.rb index c986fa6a1..cd0e4d670 100644 --- a/lib/devices/masternaut.rb +++ b/lib/devices/masternaut.rb @@ -154,7 +154,7 @@ def send_route(customer, route, _options = {}) stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil, stop.detail, stop.comment, - stop.phone_number, + stop.phone_number ].compact.join(' ').strip, time: stop.time, updated_at: stop.base_updated_at, diff --git a/lib/devices/praxedo.rb b/lib/devices/praxedo.rb index 52a328642..c617981ac 100644 --- a/lib/devices/praxedo.rb +++ b/lib/devices/praxedo.rb @@ -140,7 +140,7 @@ def send_route(customer, route, _options = {}) [ stop.comment, stop.time_window_start_1 || stop.time_window_end_1 ? (stop.time_window_start_1 ? stop.time_window_start_1_time + number_of_days(stop.time_window_start_1) : '') + (stop.time_window_start_1 && stop.time_window_end_1 ? '-' : '') + (stop.time_window_end_1 ? (stop.time_window_end_1_time + number_of_days(stop.time_window_end_1) || '') : '') : nil, - stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil, + stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil ].compact.join(' ').strip code_type_inter = stop.visit.tags.map(&:label).map { |label| label.split('praxedo:')[1] }.compact.first events << format_position(customer, route, stop, order_id, stop: true, appointment_time: stop.time_window_start_1 || stop.time_window_end_1 || stop.time_window_start_2 || stop.time_window_end_2 || stop.time, schedule_time: stop.time, duration: stop.duration, description: description, code_type_inter: code_type_inter, instructions: [{id: customer.devices[:praxedo][:code_route], value: code_route_id}, {id: customer.devices[:praxedo][:code_mat], value: stop.visit.ref}], equipment_name: stop.visit.destination.ref) # Destination ref is used as equipmentName, best possible choice @@ -257,7 +257,7 @@ def clear_route(customer, route) description = [ stop.comment, stop.time_window_start_1 || stop.time_window_end_1 ? (stop.time_window_start_1 ? stop.time_window_start_1_time + number_of_days(stop.time_window_start_1) : '') + (stop.time_window_start_1 && stop.time_window_end_1 ? '-' : '') + (stop.time_window_end_1 ? (stop.time_window_end_1_time + number_of_days(stop.time_window_end_1) || '') : '') : nil, - stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil, + stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil ].compact.join(' ').strip events[:eventsToDelete] << encode_uid(description, order_id, planning_date(route.planning)) end diff --git a/lib/devices/tomtom.rb b/lib/devices/tomtom.rb index e13ba5bda..83f9eace7 100644 --- a/lib/devices/tomtom.rb +++ b/lib/devices/tomtom.rb @@ -196,7 +196,7 @@ def send_route(customer, route, options = {}) stop.time_window_start_2 || stop.time_window_end_2 ? (stop.time_window_start_2 ? stop.time_window_start_2_time + number_of_days(stop.time_window_start_2) : '') + (stop.time_window_start_2 && stop.time_window_end_2 ? '-' : '') + (stop.time_window_end_2 ? (stop.time_window_end_2_time + number_of_days(stop.time_window_end_2) || '') : '') : nil, stop.detail, stop.comment, - stop.phone_number, + stop.phone_number ].compact.join(' ').strip send_destination_order customer, route, position, (stop.is_a?(StopVisit) ? "v#{stop.visit_id}" : "r#{stop.id}"), description, stop.time end diff --git a/lib/optim/optimizer_wrapper.rb b/lib/optim/optimizer_wrapper.rb index dc784a8a8..9fb270e1c 100644 --- a/lib/optim/optimizer_wrapper.rb +++ b/lib/optim/optimizer_wrapper.rb @@ -275,7 +275,7 @@ def build_services(planning, routes, stops, **options) # For the optimization setup duration starts before the time window start start: stop.time_window_start_2 && (stop.time_window_start_2 + stop.destination_duration), end: stop.time_window_end_2 && (stop.time_window_end_2 + extra_time) - }, + } ].compact, duration: stop.duration, setup_duration: stop.destination_duration diff --git a/test/test_helper.rb b/test/test_helper.rb index 2501f4bf1..519ef709e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -20,7 +20,7 @@ require "minitest/reporters" Minitest::Reporters.use! [ - Minitest::Reporters::ProgressReporter.new, + Minitest::Reporters::ProgressReporter.new #Minitest::Reporters::HtmlReporter.new # create html reporte with many more informations ] From 802e453b2694191e11311b0a72f3793ba787f996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 16:20:16 +0200 Subject: [PATCH 17/24] fix assigned if spacing --- .rubocop_todo.yml | 2 +- app/api/v01/destinations.rb | 50 ++++++++++--------- app/api/v01/routes_get.rb | 30 +++++------ app/api/v01/vehicle_usage_sets.rb | 11 ++-- app/api/v01/vehicle_usages.rb | 11 ++-- app/api/v01/vehicles.rb | 15 +++--- app/api/v01/visits_get.rb | 15 +++--- app/api/v01/zonings.rb | 37 +++++++------- app/api/v100/destinations.rb | 26 +++++----- .../api_web/v01/destinations_controller.rb | 23 +++++---- .../api_web/v01/stores_controller.rb | 23 +++++---- .../api_web/v01/zones_controller.rb | 28 ++++++----- app/controllers/destinations_controller.rb | 11 ++-- app/helpers/routes_helper.rb | 11 ++-- app/jobs/clustering.rb | 15 +++--- app/jobs/importer_destinations.rb | 11 ++-- app/jobs/importer_vehicle_usage_sets.rb | 11 ++-- app/models/planning.rb | 23 +++++---- app/models/vehicle.rb | 11 ++-- .../api_web/v01/zones/index.json.jbuilder | 15 +++--- lib/devices/fleet.rb | 13 ++--- lib/optim/optimizer_wrapper.rb | 14 +++--- 22 files changed, 217 insertions(+), 189 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cbf9def3f..dae3e4a51 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -864,7 +864,7 @@ Lint/Void: # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 336 + Max: 350 # Offense count: 32 # Configuration parameters: CountBlocks, CountModifierForms. diff --git a/app/api/v01/destinations.rb b/app/api/v01/destinations.rb index 6c1a071ae..0348de29d 100644 --- a/app/api/v01/destinations.rb +++ b/app/api/v01/destinations.rb @@ -66,14 +66,14 @@ def destination_params end def present_geojson_destinations(params) - destinations = if params.key?(:ids) - ids = params[:ids].split(',') - current_customer.destinations.includes_visits.select{ |destination| - params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } - } - else - current_customer.destinations.includes_visits - end + destinations = + if params.key?(:ids) + current_customer.destinations.includes_visits.select{ |destination| + params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } + } + else + current_customer.destinations.includes_visits + end '{"type":"FeatureCollection","features":[' + destinations.select(&:position?).map { |d| feat = { type: 'Feature', @@ -137,13 +137,14 @@ def with_quantities(visit) if env['api.format'] == :geojson present_geojson_destinations params else - destinations = if params.key?(:ids) - current_customer.destinations.includes_visits.select{ |destination| - params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } - } - else - current_customer.destinations.includes_visits.load - end + destinations = + if params.key?(:ids) + current_customer.destinations.includes_visits.select{ |destination| + params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } + } + else + current_customer.destinations.includes_visits.load + end present destinations, with: V01::Entities::Destination end end @@ -228,16 +229,17 @@ def with_quantities(visit) end params[:planning].delete(:zoning_ids) end - import = if params[:destinations] - # FIXME ImportJSON has its own conversion methods. It should be done at the API level - ImportJson.new(importer: ImporterDestinations.new(current_customer, params[:planning]), replace: params[:replace], json: import_destination_params) - elsif params[:remote] - case params[:remote] - when :tomtom then ImportTomtom.new(importer: ImporterDestinations.new(current_customer, params[:planning]), customer: current_customer, replace: params[:replace]) + import = + if params[:destinations] + # FIXME ImportJSON has its own conversion methods. It should be done at the API level + ImportJson.new(importer: ImporterDestinations.new(current_customer, params[:planning]), replace: params[:replace], json: import_destination_params) + elsif params[:remote] + case params[:remote] + when :tomtom then ImportTomtom.new(importer: ImporterDestinations.new(current_customer, params[:planning]), customer: current_customer, replace: params[:replace]) + end + else + ImportCsv.new(importer: ImporterDestinations.new(current_customer, params[:planning]), replace: params[:replace], file: params[:file]) end - else - ImportCsv.new(importer: ImporterDestinations.new(current_customer, params[:planning]), replace: params[:replace], file: params[:file]) - end if import && import.valid? && (destinations = import.import(true)) case params[:remote] diff --git a/app/api/v01/routes_get.rb b/app/api/v01/routes_get.rb index e057059b4..636d2480c 100644 --- a/app/api/v01/routes_get.rb +++ b/app/api/v01/routes_get.rb @@ -57,13 +57,14 @@ def present_routes(routes, params) optional :quantities, type: Boolean, default: false, desc: 'Include the quantities when using geojson output.' end get do - routes = if params.key?(:ids) - current_customer.plannings.flat_map(&:routes).select{ |route| - params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } - } - else - current_customer.plannings.flat_map{ |p| p.routes.includes_vehicle_usages.load } - end + routes = + if params.key?(:ids) + current_customer.plannings.flat_map(&:routes).select{ |route| + params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } + } + else + current_customer.plannings.flat_map{ |p| p.routes.includes_vehicle_usages.load } + end present_routes routes, params end end @@ -87,13 +88,14 @@ def present_routes(routes, params) end get do planning_id = ParseIdsRefs.read(params[:planning_id]) - routes = if params.key?(:ids) - current_customer.plannings.where(planning_id).first!.routes.select{ |route| - params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } - } - else - current_customer.plannings.where(planning_id).first!.routes.includes_vehicle_usages.load - end + routes = + if params.key?(:ids) + current_customer.plannings.where(planning_id).first!.routes.select{ |route| + params[:ids].any?{ |s| ParseIdsRefs.match(s, route) } + } + else + current_customer.plannings.where(planning_id).first!.routes.includes_vehicle_usages.load + end present_routes routes, params end diff --git a/app/api/v01/vehicle_usage_sets.rb b/app/api/v01/vehicle_usage_sets.rb index 36abe27b3..fdf877112 100644 --- a/app/api/v01/vehicle_usage_sets.rb +++ b/app/api/v01/vehicle_usage_sets.rb @@ -41,11 +41,12 @@ def vehicle_usage_set_params optional :ids, type: Array[Integer], desc: 'Select returned vehicle_usage_sets by id.', coerce_with: CoerceArrayInteger end get do - vehicle_usage_sets = if params.key?(:ids) - current_customer.vehicle_usage_sets.select{ |vehicle_usage_set| params[:ids].include?(vehicle_usage_set.id) } - else - current_customer.vehicle_usage_sets.load - end + vehicle_usage_sets = + if params.key?(:ids) + current_customer.vehicle_usage_sets.select{ |vehicle_usage_set| params[:ids].include?(vehicle_usage_set.id) } + else + current_customer.vehicle_usage_sets.load + end present vehicle_usage_sets, with: V01::Entities::VehicleUsageSet end diff --git a/app/api/v01/vehicle_usages.rb b/app/api/v01/vehicle_usages.rb index 43419f246..aaef8b219 100644 --- a/app/api/v01/vehicle_usages.rb +++ b/app/api/v01/vehicle_usages.rb @@ -46,11 +46,12 @@ def vehicle_usage_params end get do vehicle_usage_set = current_customer.vehicle_usage_sets.where(id: params[:vehicle_usage_set_id]).first - vehicle_usages = if vehicle_usage_set && params.key?(:ids) - vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| params[:ids].include?(vehicle_usage.id) } - else - vehicle_usage_set.vehicle_usages.load - end + vehicle_usages = + if vehicle_usage_set && params.key?(:ids) + vehicle_usage_set.vehicle_usages.select{ |vehicle_usage| params[:ids].include?(vehicle_usage.id) } + else + vehicle_usage_set.vehicle_usages.load + end if vehicle_usage_set && vehicle_usages present vehicle_usages, with: V01::Entities::VehicleUsageWithVehicle else diff --git a/app/api/v01/vehicles.rb b/app/api/v01/vehicles.rb index 55ce60a86..c94760682 100644 --- a/app/api/v01/vehicles.rb +++ b/app/api/v01/vehicles.rb @@ -95,13 +95,14 @@ def deliverables_by_vehicle_params optional :ids, type: Array[String], desc: 'Select returned vehicles by id separated with comma. You can specify ref (not containing comma) instead of id, in this case you have to add "ref:" before each ref, e.g. ref:ref1,ref:ref2,ref:ref3.', coerce_with: CoerceArrayString end get do - vehicles = if params.key?(:ids) - current_customer.vehicles.select{ |vehicle| - params[:ids].any?{ |s| ParseIdsRefs.match(s, vehicle) } - } - else - current_customer.vehicles.load - end + vehicles = + if params.key?(:ids) + current_customer.vehicles.select{ |vehicle| + params[:ids].any?{ |s| ParseIdsRefs.match(s, vehicle) } + } + else + current_customer.vehicles.load + end present vehicles, with: V01::Entities::Vehicle end diff --git a/app/api/v01/visits_get.rb b/app/api/v01/visits_get.rb index acce26706..61f4ec909 100644 --- a/app/api/v01/visits_get.rb +++ b/app/api/v01/visits_get.rb @@ -37,13 +37,14 @@ class V01::VisitsGet < Grape::API optional :quantities, type: Boolean, default: false, desc: 'Include the quantities when using geojson output.' end get do - visits = if params.key?(:ids) - current_customer.visits.select{ |visit| - params[:ids].any?{ |s| ParseIdsRefs.match(s, visit) } - } - else - current_customer.visits - end + visits = + if params.key?(:ids) + current_customer.visits.select{ |visit| + params[:ids].any?{ |s| ParseIdsRefs.match(s, visit) } + } + else + current_customer.visits + end if env['api.format'] == :geojson '{"type":"FeatureCollection","features":[' + visits.map { |visit| if visit.destination.position? diff --git a/app/api/v01/zonings.rb b/app/api/v01/zonings.rb index f1ba63eb2..571c385a0 100644 --- a/app/api/v01/zonings.rb +++ b/app/api/v01/zonings.rb @@ -46,11 +46,12 @@ def zoning_params(d_params = nil) optional :ids, type: Array[Integer], desc: 'Select returned zonings by id.', coerce_with: CoerceArrayInteger end get do - zonings = if params.key?(:ids) - current_customer.zonings.select{ |zoning| params[:ids].include?(zoning.id) } - else - current_customer.zonings.load - end + zonings = + if params.key?(:ids) + current_customer.zonings.select{ |zoning| params[:ids].include?(zoning.id) } + else + current_customer.zonings.load + end present zonings, with: V01::Entities::Zoning end @@ -183,12 +184,13 @@ def zoning_params(d_params = nil) patch ':id/isochrone' do Zoning.transaction do zoning = current_customer.zonings.where(id: params[:id]).first! - vehicle_usage_set = if params.key?(:vehicle_usage_set_id) - vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) - current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } - else - current_customer.vehicle_usage_sets[0] - end + vehicle_usage_set = + if params.key?(:vehicle_usage_set_id) + vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) + current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } + else + current_customer.vehicle_usage_sets[0] + end size = Integer(params[:size]) if vehicle_usage_set zoning.isochrones(size, vehicle_usage_set, params[:departure_date]) @@ -246,12 +248,13 @@ def zoning_params(d_params = nil) patch ':id/isodistance' do Zoning.transaction do zoning = current_customer.zonings.where(id: params[:id]).first! - vehicle_usage_set = if params.key?(:vehicle_usage_set_id) - vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) - current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } - else - current_customer.vehicle_usage_sets[0] - end + vehicle_usage_set = + if params.key?(:vehicle_usage_set_id) + vehicle_usage_set_id = Integer(params[:vehicle_usage_set_id]) + current_customer.vehicle_usage_sets.to_a.find{ |vehicle_usage_set| vehicle_usage_set.id == vehicle_usage_set_id } + else + current_customer.vehicle_usage_sets[0] + end size = Integer(params[:size]) if vehicle_usage_set zoning.prefered_unit = @current_user.prefered_unit diff --git a/app/api/v100/destinations.rb b/app/api/v100/destinations.rb index 2dd5d834b..bb72541a1 100644 --- a/app/api/v100/destinations.rb +++ b/app/api/v100/destinations.rb @@ -68,19 +68,21 @@ class V100::Destinations < Grape::API if env['api.format'] == :geojson present_geojson_destinations params else - destinations = if params[:visits] - current_customer.destinations.includes_visits - else - current_customer.destinations - end + destinations = + if params[:visits] + current_customer.destinations.includes_visits + else + current_customer.destinations + end - destinations = if params.key?(:ids) - destinations.select{ |destination| - params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } - } - else - destinations.load - end + destinations = + if params.key?(:ids) + destinations.select{ |destination| + params[:ids].any?{ |s| ParseIdsRefs.match(s, destination) } + } + else + destinations.load + end if params[:visits] present destinations, with: V100::Entities::DestinationWithVisit else diff --git a/app/controllers/api_web/v01/destinations_controller.rb b/app/controllers/api_web/v01/destinations_controller.rb index 1745b1976..bc99f7c61 100644 --- a/app/controllers/api_web/v01/destinations_controller.rb +++ b/app/controllers/api_web/v01/destinations_controller.rb @@ -24,19 +24,20 @@ class ApiWeb::V01::DestinationsController < ApiWeb::V01::ApiWebController def index @customer = current_user.customer - @destinations = if params.key?(:ids) - ids = params[:ids].split(',') - current_user.customer.destinations.where(ParseIdsRefs.where(Destination, ids)).includes_visits - else - respond_to do |format| - format.html do - nil - end - format.json do - current_user.customer.destinations.includes_visits + @destinations = + if params.key?(:ids) + ids = params[:ids].split(',') + current_user.customer.destinations.where(ParseIdsRefs.where(Destination, ids)).includes_visits + else + respond_to do |format| + format.html do + nil + end + format.json do + current_user.customer.destinations.includes_visits + end end end - end if params.key?(:store_ids) @stores = current_user.customer.stores.where(ParseIdsRefs.where(Store, params[:store_ids].split(','))) end diff --git a/app/controllers/api_web/v01/stores_controller.rb b/app/controllers/api_web/v01/stores_controller.rb index 714100983..d91e86a80 100644 --- a/app/controllers/api_web/v01/stores_controller.rb +++ b/app/controllers/api_web/v01/stores_controller.rb @@ -22,19 +22,20 @@ class ApiWeb::V01::StoresController < ApiWeb::V01::ApiWebController def index @customer = current_user.customer - @stores = if params.key?(:ids) - ids = params[:ids].split(',') - current_user.customer.stores.where(ParseIdsRefs.where(Store, ids)) - else - respond_to do |format| - format.html do - nil - end - format.json do - current_user.customer.stores.load + @stores = + if params.key?(:ids) + ids = params[:ids].split(',') + current_user.customer.stores.where(ParseIdsRefs.where(Store, ids)) + else + respond_to do |format| + format.html do + nil + end + format.json do + current_user.customer.stores.load + end end end - end @tags = current_user.customer.tags @method = request.method_symbol end diff --git a/app/controllers/api_web/v01/zones_controller.rb b/app/controllers/api_web/v01/zones_controller.rb index 7c9bc24a9..19eec1a16 100644 --- a/app/controllers/api_web/v01/zones_controller.rb +++ b/app/controllers/api_web/v01/zones_controller.rb @@ -24,12 +24,13 @@ class ApiWeb::V01::ZonesController < ApiWeb::V01::ApiWebController def index @customer = current_user.customer - @zones = if params.key?(:ids) - ids = params[:ids].split(',') - @zoning.zones.select{ |zone| ids.include?(zone.id.to_s) } - else - @zoning.zones - end + @zones = + if params.key?(:ids) + ids = params[:ids].split(',') + @zoning.zones.select{ |zone| ids.include?(zone.id.to_s) } + else + @zoning.zones + end if params.key?(:destination_ids) destination_ids = params[:destination_ids].split(',') @destinations = current_user.customer.destinations.where(ParseIdsRefs.where(Destination, destination_ids)) @@ -40,13 +41,14 @@ def index if params.key?(:store_ids) @stores = current_user.customer.stores.where(ParseIdsRefs.where(Store, params[:store_ids].split(','))) end - @vehicle_usage_set = if params[:vehicle_usage_set_id] - current_user.customer.vehicle_usage_sets.find(params[:vehicle_usage_set_id]) - elsif params[:planning_id] - current_user.customer.plannings.find(params[:planning_id]).vehicle_usage_set - elsif current_user.customer.vehicle_usage_sets.size == 1 - current_user.customer.vehicle_usage_sets.first - end + @vehicle_usage_set = + if params[:vehicle_usage_set_id] + current_user.customer.vehicle_usage_sets.find(params[:vehicle_usage_set_id]) + elsif params[:planning_id] + current_user.customer.plannings.find(params[:planning_id]).vehicle_usage_set + elsif current_user.customer.vehicle_usage_sets.size == 1 + current_user.customer.vehicle_usage_sets.first + end @method = request.method_symbol end end diff --git a/app/controllers/destinations_controller.rb b/app/controllers/destinations_controller.rb index b70ef61e3..48a3bb839 100644 --- a/app/controllers/destinations_controller.rb +++ b/app/controllers/destinations_controller.rb @@ -31,11 +31,12 @@ class DestinationsController < ApplicationController def index @customer = current_user.customer - @destinations = if request.format.html? || !@customer.is_editable? - current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes([:tags]) - else - current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes_visits - end + @destinations = + if request.format.html? || !@customer.is_editable? + current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes([:tags]) + else + current_user.customer.destinations.reorder(Arel.sql("CASE WHEN lat IS NULL THEN 0 ELSE 1 END, geocoding_accuracy ASC NULLS LAST")).includes_visits + end @tags = current_user.customer.tags respond_to do |format| format.html diff --git a/app/helpers/routes_helper.rb b/app/helpers/routes_helper.rb index 7f172e88d..1cb726696 100644 --- a/app/helpers/routes_helper.rb +++ b/app/helpers/routes_helper.rb @@ -105,11 +105,12 @@ def route_devices(devices, route) devices_route.each do |key, value| if devices && devices.key?(key) - match_device = if value.is_a?(Array) - { items: devices[key].select{ |dv| value.include? dv[:id] } } - else - devices[key].find{ |dv| dv[:id] == value } - end + match_device = + if value.is_a?(Array) + { items: devices[key].select{ |dv| value.include? dv[:id] } } + else + devices[key].find{ |dv| dv[:id] == value } + end route_devices_hash[key] = match_device unless match_device.nil? || match_device.empty? else route_devices_hash[key] = value diff --git a/app/jobs/clustering.rb b/app/jobs/clustering.rb index 6a26276c1..283645c71 100644 --- a/app/jobs/clustering.rb +++ b/app/jobs/clustering.rb @@ -94,13 +94,14 @@ def self.hull(factory, cluster, own_multi_point, other_multi_points, buffer) points = borders.collect{ |i| factory.point(i[0], i[1]) } - concave_hull = if points.size == 1 - points[0] - elsif points.size == 2 - factory.line_string(points) - else - factory.polygon(factory.linear_ring(points)) - end + concave_hull = + if points.size == 1 + points[0] + elsif points.size == 2 + factory.line_string(points) + else + factory.polygon(factory.linear_ring(points)) + end convex_hull = own_multi_point.convex_hull diff --git a/app/jobs/importer_destinations.rb b/app/jobs/importer_destinations.rb index 97504a0d1..015c9b42b 100644 --- a/app/jobs/importer_destinations.rb +++ b/app/jobs/importer_destinations.rb @@ -847,11 +847,12 @@ def prepare_visit_with_destination_ref(row, line, destination, destination_index if row[:without_visit].nil? || row[:without_visit].strip.empty? if destination ref_planning = row[:planning_ref].blank? ? nil : row[:planning_ref].downcase - visit = if row[:ref_visit] || @nil_visit_available[ref_planning][row[:ref]] - # If nil_visit available retrieve the first visit of the destination with a nil ref_visit - @nil_visit_available[ref_planning][row[:ref]] = false - @existing_visits_by_ref[row[:ref]][row[:ref_visit]] - end + visit = + if row[:ref_visit] || @nil_visit_available[ref_planning][row[:ref]] + # If nil_visit available retrieve the first visit of the destination with a nil ref_visit + @nil_visit_available[ref_planning][row[:ref]] = false + @existing_visits_by_ref[row[:ref]][row[:ref_visit]] + end @destinations_visits_attributes_by_ref[destination.ref] ||= Hash.new visit_attributes.merge!(destination_id: destination.id) if visit diff --git a/app/jobs/importer_vehicle_usage_sets.rb b/app/jobs/importer_vehicle_usage_sets.rb index 4296fc57a..de2af2295 100644 --- a/app/jobs/importer_vehicle_usage_sets.rb +++ b/app/jobs/importer_vehicle_usage_sets.rb @@ -175,11 +175,12 @@ def import_row(_name, row, _line, options) [:tags, :tags_vehicle].each{ |key| prepare_tags(row, key) } # For each vehicle, create vehicle and vehicle usage - vehicle = if !row[:ref_vehicle].nil? && !row[:ref_vehicle].strip.empty? && @vehicles_by_ref[row[:ref_vehicle].strip] - @vehicles_by_ref[row[:ref_vehicle].strip] - else - @vehicles_without_ref.shift - end + vehicle = + if !row[:ref_vehicle].nil? && !row[:ref_vehicle].strip.empty? && @vehicles_by_ref[row[:ref_vehicle].strip] + @vehicles_by_ref[row[:ref_vehicle].strip] + else + @vehicles_without_ref.shift + end if options[:replace_vehicles] vehicle_attributes = row.slice(*columns_vehicle.keys, :capacities, :custom_attributes) diff --git a/app/models/planning.rb b/app/models/planning.rb index c68514282..a72e9f0be 100644 --- a/app/models/planning.rb +++ b/app/models/planning.rb @@ -766,17 +766,18 @@ def fetch_stops_status true else if DeviceBase.is_fleet_hash?(s) - attr = if DeviceBase.is_arrival?(s) - { - arrival_eta: s[:eta], - arrival_status: s[:status] - } - else - { - departure_eta: s[:eta], - departure_status: s[:status] - } - end + attr = + if DeviceBase.is_arrival?(s) + { + arrival_eta: s[:eta], + arrival_status: s[:status] + } + else + { + departure_eta: s[:eta], + departure_status: s[:status] + } + end route = routes.select { |r| r.id == s[:route_id].to_i }.first route && route.update(attr) end diff --git a/app/models/vehicle.rb b/app/models/vehicle.rb index f033c6a4a..dbeee6ef8 100644 --- a/app/models/vehicle.rb +++ b/app/models/vehicle.rb @@ -148,11 +148,12 @@ def default_router_dimension def default_router_options default_router.options.each do |key, value| @current_router_options ||= {} - @current_router_options[key.to_s] = if router_options[key.to_s].nil? - customer.router_options[key.to_s] - else - router_options[key.to_s] - end + @current_router_options[key.to_s] = + if router_options[key.to_s].nil? + customer.router_options[key.to_s] + else + router_options[key.to_s] + end end if !@current_router_options @current_router_options ||= {} diff --git a/app/views/api_web/v01/zones/index.json.jbuilder b/app/views/api_web/v01/zones/index.json.jbuilder index 9ff67485e..90627f7b2 100644 --- a/app/views/api_web/v01/zones/index.json.jbuilder +++ b/app/views/api_web/v01/zones/index.json.jbuilder @@ -2,13 +2,14 @@ if @vehicle_usage_set vehicle_vehicle_usages = Hash[@vehicle_usage_set.vehicle_usages.collect{ |vehicle_usage| [vehicle_usage.vehicle, vehicle_usage] }] vehicle_usages = @zones.select(&:vehicle).collect{ |zone| vehicle_vehicle_usages[zone.vehicle] } end -stores = if @stores - @stores -elsif vehicle_usages - (vehicle_usages.collect(&:default_store_start) + vehicle_usages.collect(&:default_store_stop) + vehicle_usages.collect(&:default_store_rest)).compact.uniq -else - @zoning.customer.stores -end +stores = + if @stores + @stores + elsif vehicle_usages + (vehicle_usages.collect(&:default_store_start) + vehicle_usages.collect(&:default_store_stop) + vehicle_usages.collect(&:default_store_rest)).compact.uniq + else + @zoning.customer.stores + end json.stores stores do |store| json.extract! store, :id, :name, :street, :postalcode, :city, :country, :lat, :lng, :color, :icon, :icon_size end diff --git a/lib/devices/fleet.rb b/lib/devices/fleet.rb index aef7a6834..928b2cf70 100644 --- a/lib/devices/fleet.rb +++ b/lib/devices/fleet.rb @@ -453,12 +453,13 @@ def generate_store_id(store, route, date, options) end def generate_mission_id(stop, date) - order_id = if stop.is_a?(StopVisit) - ref = [stop.visit.ref, stop.ref].compact.join('-') - (ref.blank? ? '' : ref + '-') + "v#{stop.visit_id}" - else - "r#{stop.id}" - end + order_id = + if stop.is_a?(StopVisit) + ref = [stop.visit.ref, stop.ref].compact.join('-') + (ref.blank? ? '' : ref + '-') + "v#{stop.visit_id}" + else + "r#{stop.id}" + end "mission-#{order_id}-#{date.strftime('%Y_%m_%d')}-#{stop.route.id}" end diff --git a/lib/optim/optimizer_wrapper.rb b/lib/optim/optimizer_wrapper.rb index 9fb270e1c..bd1e04203 100644 --- a/lib/optim/optimizer_wrapper.rb +++ b/lib/optim/optimizer_wrapper.rb @@ -168,12 +168,14 @@ def solve(vrp, progress, key) def build_configuration(**options) service_ratio = options[:moving_stop_ids]&.any? && options[:service_count].to_i > 0 ? options[:moving_stop_ids].size.to_f / options[:service_count] : 1 service_ratio = [service_ratio, 0.2].max - optim_duration_min = if options[:optimize_minimal_time] - (service_ratio * options[:optimize_minimal_time] * options[:vehicle_count] * 1000).to_i - end - optim_duration_max = if options[:optimize_time] - (service_ratio * options[:optimize_time] * options[:vehicle_count]).to_i - end + optim_duration_min = + if options[:optimize_minimal_time] + (service_ratio * options[:optimize_minimal_time] * options[:vehicle_count] * 1000).to_i + end + optim_duration_max = + if options[:optimize_time] + (service_ratio * options[:optimize_time] * options[:vehicle_count]).to_i + end { preprocessing: { max_split_size: options[:max_split_size], From c4df553a8a2d049e5353c0327720fa090ec971f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 16:29:37 +0200 Subject: [PATCH 18/24] Bump rubocop --- Gemfile.lock | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3c152b1dd..4272ecccf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,7 +157,7 @@ GEM amoeba (3.3.0) activerecord (>= 5.2.0) ansi (1.5.0) - ast (2.4.2) + ast (2.4.3) auto_strip_attributes (2.6.0) activerecord (>= 4.0) awesome_print (1.9.2) @@ -411,12 +411,12 @@ GEM turbolinks jquery-ui-rails (5.0.5) railties (>= 3.2.16) - json (2.7.2) + json (2.13.0) json-schema (4.3.1) addressable (>= 2.8) jwt (2.8.2) base64 - language_server-protocol (3.17.0.3) + language_server-protocol (3.17.0.5) launchy (3.0.1) addressable (~> 2.8) childprocess (~> 5.0) @@ -438,6 +438,7 @@ GEM rexml libnotify (0.9.4) ffi (>= 1.0.11) + lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -526,8 +527,8 @@ GEM orm_adapter (0.5.0) pagy (9.1.0) paloma (6.1.0) - parallel (1.25.1) - parser (3.3.4.0) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc pg (1.5.6) @@ -541,6 +542,7 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) + prism (1.4.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -623,7 +625,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) - regexp_parser (2.9.2) + regexp_parser (2.10.0) reline (0.5.9) io-console (~> 0.5) request_store (1.7.0) @@ -636,7 +638,7 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.9) + rexml (3.4.1) rgeo (3.0.1) rgeo-activerecord (7.0.1) activerecord (>= 5.0) @@ -670,19 +672,20 @@ GEM json-schema (>= 2.2, < 5.0) railties (>= 3.1, < 7.2) rspec-core (>= 2.14) - rubocop (1.65.0) + rubocop (1.78.0) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) ruby-progressbar (1.13.0) ruby-vips (2.2.2) ffi (~> 1.12) @@ -791,7 +794,7 @@ GEM concurrent-ruby (~> 1.0) uglifier (3.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uniform_notifier (1.16.0) uri (1.0.3) validates_timeliness (6.0.1) From 74669d250a11b3ae27b78f04e38e70af8a4bf424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 17:44:56 +0200 Subject: [PATCH 19/24] Fix lint --- app/api/v01/plannings_get.rb | 2 +- config/environments/production.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/v01/plannings_get.rb b/app/api/v01/plannings_get.rb index c75b43e72..a54c4126c 100644 --- a/app/api/v01/plannings_get.rb +++ b/app/api/v01/plannings_get.rb @@ -81,7 +81,7 @@ def get_format_routes_email(planning_ids) plannings = plannings.select{ |plan| params[:ids].any?{ |s| ParseIdsRefs.match(s, plan) } } if params.key?(:ids) if env['api.format'] == :ics - if params.key?(:email) && YAML.load(params[:email]) + if params.key?(:email) && YAML.safe_load(params[:email]) planning_ids = plannings.map(&:id) emails_routes = get_format_routes_email(planning_ids) route_calendar_email(emails_routes) diff --git a/config/environments/production.rb b/config/environments/production.rb index e82c2c96f..9e507295a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,5 +1,5 @@ Rails.application.configure do - config.webpacker.check_yarn_integrity = false # Settings specified here will take precedence over those in config/application.rb. + config.webpacker.check_yarn_integrity = false # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = false From 88c8c3e2672cd376a89c6e778dfe2d93d4a3ecee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 15:00:05 +0200 Subject: [PATCH 20/24] Fix DuplicateBranch offense --- app/controllers/concerns/link_back.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/concerns/link_back.rb b/app/controllers/concerns/link_back.rb index 468c65aca..cabf888ce 100644 --- a/app/controllers/concerns/link_back.rb +++ b/app/controllers/concerns/link_back.rb @@ -21,10 +21,7 @@ def save_link_back if referer_uri && params['back'] session[:link_back] = referer_uri.path elsif referer_uri && referer_params && referer_params['back'] - # Clear link_back if we're coming from a page with back=true to prevent infinite loops - session.delete(:link_back) - elsif !(referer_uri && referer_params && referer_params['back']) - # Clear link_back if we're not coming from a page with back=true + # Clear link_back session.delete(:link_back) end end From 26ea87168b53c98b01a3a1810092daf773a6bd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 15:06:21 +0200 Subject: [PATCH 21/24] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9091806d4..0b518e29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Routes: - Add stores to scopes [#435](https://github.com/cartoway/planner-web/pull/435) - Ensure compute_saved! is in a transaction [#435](https://github.com/cartoway/planner-web/pull/435) + - Update rubocop rules [#440](https://github.com/cartoway/planner-web/pull/440) ### Removed From afc4c2e36c76488bee48f2a32582ab48b2da1be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 23 Jul 2025 14:30:42 +0200 Subject: [PATCH 22/24] Introduce with_lock_on on planning api calls --- app/api/v01/api.rb | 3 + app/api/v01/plannings.rb | 90 ++++++++++++----------- app/api/v100/api.rb | 3 + app/controllers/application_controller.rb | 1 + config/locales/en.yml | 2 + config/locales/fr.yml | 2 + lib/locking_utils.rb | 24 ++++++ test/services/locking_utils_test.rb | 52 +++++++++++++ 8 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 lib/locking_utils.rb create mode 100644 test/services/locking_utils_test.rb diff --git a/app/api/v01/api.rb b/app/api/v01/api.rb index 1ff802db3..8e08473f7 100644 --- a/app/api/v01/api.rb +++ b/app/api/v01/api.rb @@ -16,8 +16,11 @@ # # require 'exceptions' +require 'locking_utils' class V01::Api < Grape::API + helpers LockingUtils + helpers do def session env[Rack::RACK_SESSION] diff --git a/app/api/v01/plannings.rb b/app/api/v01/plannings.rb index ace958c69..2e3cac934 100644 --- a/app/api/v01/plannings.rb +++ b/app/api/v01/plannings.rb @@ -73,17 +73,20 @@ def planning_params end put ':id' do planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! - planning.update! planning_params - - if params[:routes] && !params[:routes].empty? - param_routes = params[:routes].collect { |route| [route[:id], route.to_h.except(:id)] }.to_h - routes = planning.routes.select{ |r| param_routes.include? r.id } - routes.each do |route| - route.update!(param_routes[route.id]) + routes = planning.routes + with_locks_on(planning, *routes) do + planning.update! planning_params + + if params[:routes] && !params[:routes].empty? + param_routes = params[:routes].collect { |route| [route[:id], route.to_h.except(:id)] }.to_h + routes = planning.routes.select{ |r| param_routes.include? r.id } + routes.each do |route| + route.update!(param_routes[route.id]) + end end - end - present planning, with: V01::Entities::Planning, geojson: params[:with_geojson] + present planning, with: V01::Entities::Planning, geojson: params[:with_geojson] + end end desc 'Delete planning.', @@ -128,8 +131,10 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end patch ':id/refresh' do - Route.includes_destinations_and_stores.scoping do - planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! + planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).preload_route_details.first! + routes = planning.routes + stops = routes.flat_map(&:stops) + with_locks_on(planning, *routes, *stops) do raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) planning.compute_saved @@ -158,7 +163,9 @@ def planning_params route = planning.routes.find{ |route| route.id == Integer(params[:route_id]) } vehicle_usage = planning.vehicle_usage_set.vehicle_usages.find(params[:vehicle_usage_id]) - Planning.transaction do + vehicle_usage_route = planning.routes.find{ |route| route.vehicle_usage_id == vehicle_usage.id } + + with_locks_on(planning, route, vehicle_usage_route) do if route && vehicle_usage && planning.switch(route, vehicle_usage) && planning.save! && planning.compute && planning.save! if params[:details] || params[:with_details] present planning, with: V01::Entities::Planning, geojson: params[:with_geojson] @@ -191,8 +198,8 @@ def planning_params raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) stops = planning.routes.flat_map{ |r| r.stops }.select{ |stop| params[:stop_ids].include?(stop.id) } - begin - Planning.transaction do + with_locks_on(planning, *stops) do + begin stops.each do |stop| planning.automatic_insert(stop, max_time: params[:max_time], @@ -202,9 +209,9 @@ def planning_params end planning.compute_saved status 204 + rescue Exceptions::LoopError => e + error! V01::Status.code_response(:code_400), 400 end - rescue Exceptions::LoopError => e - error! V01::Status.code_response(:code_400), 400 end end end @@ -222,25 +229,20 @@ def planning_params optional :with_geojson, type: Symbol, values: [:true, :false, :point, :polyline], default: :false, desc: 'Fill the geojson field with route geometry: `point` to return only points, `polyline` to return with encoded linestring.' end get ':id/apply_zonings' do - returned_planning = nil - - Planning.transaction do - planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).lock(true).first! - - routes = Route.where(planning_id: planning.id).lock(true).to_a - - Stop.where(route_id: routes.map(&:id)).lock(true).to_a - - planning_with_associations = Planning.where(id: planning.id).preload_route_details.first! + planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! + routes = Route.where(planning_id: planning.id).to_a + stops = Stop.where(route_id: routes.map(&:id)).to_a + with_locks_on(planning, *routes, *stops) do raise Exceptions::JobInProgressError if Job.on_planning(planning.customer.job_optimizer, planning.id) - planning_with_associations.zoning_outdated = true - planning_with_associations.split_by_zones(nil) - planning_with_associations.compute_saved! + + planning.zoning_outdated = true + planning.split_by_zones(nil) + planning.compute_saved! end if params[:details] || params[:with_details] - present returned_planning, with: V01::Entities::Planning, geojson: params[:with_geojson] + present planning, with: V01::Entities::Planning, geojson: params[:with_geojson] else status 204 end @@ -268,18 +270,22 @@ def planning_params planning = current_customer.plannings.where(ParseIdsRefs.read(params[:id])).first! raise Exceptions::JobInProgressError if planning.customer.job_optimizer - begin - Optimizer.optimize(planning, nil, { global: params[:global], synchronous: params[:synchronous], active_only: params[:all_stops].nil? ? params[:active_only] : !params[:all_stops], ignore_overload_multipliers: params[:ignore_overload_multipliers] }) - current_customer.save! - rescue VRPNoSolutionError - error! V01::Status.code_response(:code_304), 304 - end - if params[:synchronous] && (params[:details] || params[:with_details]) - present planning, with: V01::Entities::Planning, geojson: params[:with_geojson] - elsif planning.customer.job_optimizer - present planning.customer.job_optimizer, with: V01::Entities::Job - else - status 204 + routes = planning.routes + stops = routes.flat_map(&:stops) + with_locks_on(planning, *routes, *stops) do + begin + Optimizer.optimize(planning, nil, { global: params[:global], synchronous: params[:synchronous], active_only: params[:all_stops].nil? ? params[:active_only] : !params[:all_stops], ignore_overload_multipliers: params[:ignore_overload_multipliers] }) + current_customer.save! + rescue VRPNoSolutionError + error! V01::Status.code_response(:code_304), 304 + end + if params[:synchronous] && (params[:details] || params[:with_details]) + present planning, with: V01::Entities::Planning, geojson: params[:with_geojson] + elsif planning.customer.job_optimizer + present planning.customer.job_optimizer, with: V01::Entities::Job + else + status 204 + end end rescue Exceptions::JobInProgressError status 409 diff --git a/app/api/v100/api.rb b/app/api/v100/api.rb index bc70d0ff7..531144ae4 100644 --- a/app/api/v100/api.rb +++ b/app/api/v100/api.rb @@ -1,6 +1,9 @@ require 'exceptions' +require 'locking_utils' class V100::Api < Grape::API + include LockingUtils + helpers do def session env[Rack::RACK_SESSION] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2320128b2..f76ffcd67 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,6 +16,7 @@ # # class ApplicationController < ActionController::Base + include LockingUtils # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. diff --git a/config/locales/en.yml b/config/locales/en.yml index e4e9891ce..fea022f02 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -722,6 +722,8 @@ en: deadlock: >- Failed to update data. Another update was realized simultaneously, you may have performed several operations too fast or multiple times. + locked: + resource_locked_with_id: An object %{klass} with id %{id} is being modified, please try again later. management: title: An error occurred description: The page you want to access raised an error diff --git a/config/locales/fr.yml b/config/locales/fr.yml index dbb533718..5d4f068ae 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -730,6 +730,8 @@ fr: deadlock: >- Une autre mise à jour a été effectuée en simultané, vous avez peut-être réalisé plusieurs opérations trop rapidement ou plusieurs fois. + locked: + resource_locked_with_id: Un objet %{klass} avec l'identifiant %{id} est en cours de modification, réessayez plus tard. management: title: Impossible d'accèder à la page description: La page que vous essayez d'atteindre a provoqué une erreur diff --git a/lib/locking_utils.rb b/lib/locking_utils.rb new file mode 100644 index 000000000..aa5cd532c --- /dev/null +++ b/lib/locking_utils.rb @@ -0,0 +1,24 @@ +module LockingUtils + class ResourceLockedError < StandardError; end + + # Lock all given records (can be of different models) in a single transaction. + # Raises ResourceLockedError if any lock cannot be obtained immediately. + def with_locks_on(*records) + klasses = records.flatten.group_by(&:class) + ApplicationRecord.transaction do + klasses.each do |klass, recs| + recs.each do |rec| + begin + klass.where(id: rec.id).lock('FOR UPDATE NOWAIT').first! + rescue ActiveRecord::LockWaitTimeout, ActiveRecord::StatementInvalid => e + if e.message =~ /could not obtain lock|NOWAIT/ + raise ResourceLockedError, I18n.t('errors.database.locked.resource_locked_with_id', klass: klass.name, id: rec.id) + end + raise + end + end + end + yield + end + end +end diff --git a/test/services/locking_utils_test.rb b/test/services/locking_utils_test.rb new file mode 100644 index 000000000..bec5e3e25 --- /dev/null +++ b/test/services/locking_utils_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' +require 'locking_utils' + +class LockingUtilsTest < ActiveSupport::TestCase + include LockingUtils + + setup do + @planning = plannings(:planning_one) + end + + test 'with_locks_on executes the block if the lock is available' do + executed = false + with_locks_on(@planning) do + executed = true + end + assert executed, 'The block should be executed when the lock is available' + end + + test 'with_locks_on raises an error with klass and id if another thread holds the lock' do + error = nil + t1_ready = Queue.new + t1 = Thread.new do + ActiveRecord::Base.connection_pool.with_connection do + with_locks_on(@planning) do + t1_ready.push(true) + sleep 1 + end + end + end + + t2 = Thread.new do + t1_ready.pop + begin + ActiveRecord::Base.connection_pool.with_connection do + with_locks_on(@planning) do + # should never be executed + end + end + rescue StandardError => e + error = e + end + end + + t1.join + t2.join + + assert error, 'An error should be raised if the lock is already held' + assert error.is_a?(LockingUtils::ResourceLockedError), "Expected ResourceLockedError, got: #{error.class}" + expected_message = I18n.t('errors.database.locked.resource_locked_with_id', klass: @planning.class.name, id: @planning.id) + assert_equal expected_message, error.message, "Expected error message to be '#{expected_message}', got: '#{error.message}'" + end +end From cae97ed7c189d7d588fb4b7cb9026e4edeb59da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 10:01:22 +0200 Subject: [PATCH 23/24] SimplifyJob - No ruby allocation --- app/jobs/simplify_geojson_tracks_job.rb | 62 ++++++++++++++++--------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/app/jobs/simplify_geojson_tracks_job.rb b/app/jobs/simplify_geojson_tracks_job.rb index 05028fbd4..6cb007f99 100644 --- a/app/jobs/simplify_geojson_tracks_job.rb +++ b/app/jobs/simplify_geojson_tracks_job.rb @@ -1,33 +1,49 @@ SimplifyGeojsonTracksJobStruct ||= Job.new(:customer_id, :route_id) class SimplifyGeojsonTracksJob < SimplifyGeojsonTracksJobStruct def perform - route = Route.find(route_id) - return if route.geojson_tracks.blank? + begin + Route.where(id: route_id).lock('FOR UPDATE NOWAIT').pluck(:id) - simplified_tracks = route.geojson_tracks.map do |geojson_track| - feature = JSON.parse(geojson_track) - encoded_polyline = feature['geometry']['polylines'] - - sql = "SELECT ST_AsEncodedPolyline( - ST_SimplifyPreserveTopology( - CASE - WHEN ST_NPoints(ST_LineFromEncodedPolyline('#{encoded_polyline}')) = 1 - THEN ST_MakeLine( - ST_StartPoint(ST_LineFromEncodedPolyline('#{encoded_polyline}')), - ST_StartPoint(ST_LineFromEncodedPolyline('#{encoded_polyline}')) + sql = <<~SQL + UPDATE routes + SET geojson_tracks = ( + SELECT array_agg( + ( + to_jsonb( + jsonb_set( + track::jsonb, + '{geometry,polylines}', + to_jsonb( + ( + SELECT ST_AsEncodedPolyline( + ST_SimplifyPreserveTopology( + CASE + WHEN ST_NPoints(ST_LineFromEncodedPolyline((track::jsonb->'geometry'->>'polylines')::text)) = 1 + THEN ST_MakeLine( + ST_StartPoint(ST_LineFromEncodedPolyline((track::jsonb->'geometry'->>'polylines')::text)), + ST_StartPoint(ST_LineFromEncodedPolyline((track::jsonb->'geometry'->>'polylines')::text)) + ) + ELSE ST_LineFromEncodedPolyline((track::jsonb->'geometry'->>'polylines')::text) + END, + 0.000001 + ) + ) + ) + ) + ) + )::text ) - ELSE ST_LineFromEncodedPolyline('#{encoded_polyline}') - END, - 0.000001 + ) + FROM unnest(routes.geojson_tracks) AS track ) - )" - - result = ActiveRecord::Base.connection.execute(sql).first + WHERE id = #{route_id} + AND geojson_tracks IS NOT NULL + AND array_length(geojson_tracks, 1) > 0 + SQL - feature['geometry']['polylines'] = result['st_asencodedpolyline'] - feature.to_json + ActiveRecord::Base.connection.exec_update(sql, "Simplify #{route_id}") + rescue ActiveRecord::LockWaitTimeout, ActiveRecord::StatementInvalid + # Simplifying is unnecessary if the route is locked as it is about to change end - - route.update_column(:geojson_tracks, simplified_tracks) end end From 4984832138c5a0690ee0b23a1366c0dac33f6e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 24 Jul 2025 15:14:02 +0200 Subject: [PATCH 24/24] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b518e29d..21fbe03b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Customer: Allow to order and filter the solvers to use [#422](https://github.com/cartoway/planner-web/pull/422) - Planning: Introduce a lasso to select multiple stops on the fly [#424](https://github.com/cartoway/planner-web/pull/424) - Admin: Store the solver used and the ones skipped (with reasons) on the job info [#422](https://github.com/cartoway/planner-web/pull/422) + - Implement `with_locks_on` to prevent concurrent modifications of records being processed by another process [#439](https://github.com/cartoway/planner-web/pull/439) ### Changed - Export: Rework visually the columns selector [#422](https://github.com/cartoway/planner-web/pull/422)