diff --git a/.github/workflows/rubyonrails.yml b/.github/workflows/rubyonrails.yml
index bbb752620..27775873b 100644
--- a/.github/workflows/rubyonrails.yml
+++ b/.github/workflows/rubyonrails.yml
@@ -2,9 +2,9 @@ name: "Ruby on Rails CI"
on:
push:
- branches: [master, dev]
+ branches: [master, dev, dev2]
pull_request:
- branches: [master, dev]
+ branches: [master, dev, dev2]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -78,7 +78,7 @@ jobs:
docker:
runs-on: ubuntu-latest
needs: [test, lint]
- if: github.ref == 'refs/heads/dev'
+ if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/dev2'
permissions:
contents: read
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe557d20d..ed38cd8cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- Visits
- Custom attributes are available in forms and displayed in plan stop pop-over [#338](https://github.com/cartoway/planner-web/pull/338)
- Customers: Admin have access to the `cost_fixed` transmitted during route optimization [#374](https://github.com/cartoway/planner-web/pull/374)
+ - Tags: Introduce a new page to group route date by tags [#340](https://github.com/cartoway/planner-web/pull/340)
- Vehicles: Introduce a maximum initial load associated to deliverable units [#373](https://github.com/cartoway/planner-web/pull/373)
- Planning:
- Routes: Allows to force the start time [#370](https://github.com/cartoway/planner-web/pull/370)
diff --git a/app/assets/javascripts/plannings.js b/app/assets/javascripts/plannings.js
index 94cac158d..7ed6cd5b5 100644
--- a/app/assets/javascripts/plannings.js
+++ b/app/assets/javascripts/plannings.js
@@ -2827,6 +2827,55 @@ var plannings_index = function(params) {
};
+var planning_statistics = function(params) {
+ var previousSelection = [];
+
+ $('#planning_tag_ids').select2({
+ dropdownParent: $('#planning_tag_ids').parent(),
+ closeOnSelect: false,
+ allowClear: true,
+ theme: 'bootstrap',
+ placeholder: I18n.t('web.select2.search_placeholder'),
+ templateSelection: selectFormatOption,
+ templateResult: selectFormatOption,
+ })
+ .off('select2:close select2:open select2:select select2:unselect')
+ .on('select2:open', function(e) {
+ previousSelection = $(this).val() || [];
+ })
+ .on('select2:select', function(e) {
+ selectGlobalActions($(this), e);
+ })
+ .on('select2:open', function(e) {
+ setTimeout(function() {
+ $('#route_selector .select2-results__option').each(function() {
+ var optionValue = $(this).attr('id').split('-').pop();
+ if (['clear', 'reverse', 'all'].includes(optionValue)) {
+ $(this).addClass('global');
+ }
+ });
+ }, 0);
+ })
+ .on('select2:select select2:unselect', function() {
+ updateSelectionCount('#tag_selector', '#planning_tag_ids', 'tag');
+ })
+ .on('select2:close', function(e) {
+ var selectedTags = $(this).val();
+
+ if (JSON.stringify(previousSelection)==JSON.stringify(selectedTags)) return;
+
+ $.ajax({
+ url: `/plannings/${params.planning_id}/statistics.js`,
+ data: { tag_ids: selectedTags.join(',') },
+ beforeSend: beforeSendWaiting,
+ error: function(xhr, status, error) {
+ ajaxError(xhr, status, error);
+ },
+ complete: completeAjaxMap
+ });
+ });
+};
+
Paloma.controller('Plannings', {
index: function() {
plannings_index(this.params);
@@ -2845,5 +2894,8 @@ Paloma.controller('Plannings', {
},
show: function() {
plannings_show(this.params);
+ },
+ statistics: function() {
+ planning_statistics(this.params);
}
});
diff --git a/app/assets/javascripts/scaffolds.js b/app/assets/javascripts/scaffolds.js
index 9d0ec335a..82fc3cad9 100644
--- a/app/assets/javascripts/scaffolds.js
+++ b/app/assets/javascripts/scaffolds.js
@@ -740,7 +740,7 @@ export function continuousListLoading(listRef, linkRef, loadingRef, offset) {
window.addEventListener('load', loadNextPage);
};
-export function updateSelectionCount(containerRef, selectorRef) {
+export function updateSelectionCount(containerRef, selectorRef, type = 'route') {
var $select = $(selectorRef);
var selectedValues = $select.val() || [];
@@ -751,12 +751,13 @@ export function updateSelectionCount(containerRef, selectorRef) {
var text = '';
if (selectedCount === 0) {
- text = I18n.t('web.select2.route_none');
+ text = I18n.t(`web.select2.${type}_none`);
} else if (selectedCount === 1) {
- text = "1 " + I18n.t('web.select2.route_selected');
+ text = "1 " + I18n.t(`web.select2.${type}_selected`);
} else {
- text = selectedCount + " " + I18n.t('web.select2.routes_selected');
+ text = selectedCount + " " + I18n.t(`web.select2.${type}s_selected`);
}
+
$('.select2-selection__rendered', containerRef).html(
'
' + text + ''
);
diff --git a/app/assets/stylesheets/plannings.css.scss b/app/assets/stylesheets/plannings.css.scss
index 354552a84..1cb6f905c 100644
--- a/app/assets/stylesheets/plannings.css.scss
+++ b/app/assets/stylesheets/plannings.css.scss
@@ -367,6 +367,17 @@ $sidebar-margins: 10px;
}
#planning {
+ .tag-info {
+ white-space: nowrap;
+ margin: 2px;
+ padding: 7px 6px 2px 6px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+ .fa {
+ margin-right: 4px;
+ }
+ }
.route-info {
white-space: nowrap;
margin: 2px;
diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss
index 943b337d3..4ce6d81f7 100644
--- a/app/assets/stylesheets/scaffolds.css.scss
+++ b/app/assets/stylesheets/scaffolds.css.scss
@@ -1026,3 +1026,81 @@ 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;
+ }
+}
+
+.stop-info {
+ padding: 0 2px;
+ text-align: center;
+ height: 22px;
+ margin: 0 5px;
+}
+
+.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;
+}
+
+.tag-info {
+ color: #555555;
+ background: #fff;
+ border: 1px solid $grey-color;
+ border-radius: 4px;
+ cursor: default;
+ float: left;
+ padding: 0 6px;
+}
diff --git a/app/controllers/plannings_controller.rb b/app/controllers/plannings_controller.rb
index 972d2e267..ff4d45fe2 100644
--- a/app/controllers/plannings_controller.rb
+++ b/app/controllers/plannings_controller.rb
@@ -27,7 +27,7 @@ class PlanningsController < ApplicationController
UPDATE_ACTIONS = [:update, :move, :switch, :automatic_insert, :update_stop, :active, :reverse_order, :apply_zonings, :optimize, :optimize_route]
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_planning_without_stops, only: [:data_header, :filter_routes, :modal, :sidebar, :refresh_route, :statistics]
before_action :set_driver_planning, only: [:driver_move]
before_action :check_no_existing_job, only: [:refresh, :driver_move] + UPDATE_ACTIONS
around_action :over_max_limit, only: [:create, :duplicate]
@@ -171,6 +171,48 @@ def driver_move
move_respond
end
+ def statistics
+ stat_keys = [:size_active, :emission, :distance, :duration, :drive_time, :wait_time, :visits_duration, :revenue, :total_cost, :balance]
+ @cumulated_stats = {}
+
+ tags = params['tag_ids'].present? ? current_user.customer.tags.where(id: params['tag_ids'].split(',')) : current_user.customer.tags
+ tags.each { |tag|
+ next if tag.nil?
+
+ routes = @planning.routes
+ .joins(vehicle_usage: [:vehicle])
+ .joins('LEFT JOIN tag_vehicles ON tag_vehicles.vehicle_id = vehicles.id')
+ .joins('LEFT JOIN tag_vehicle_usages ON tag_vehicle_usages.vehicle_usage_id = vehicle_usages.id')
+ .where('tag_vehicles.tag_id = ? OR tag_vehicle_usages.tag_id = ?', tag.id, tag.id)
+ .distinct
+
+ @cumulated_stats[tag.id] = { label: tag.label, tag: tag, stats: stat_init_hash }
+ routes.each { |route|
+ stat_keys.each { |key|
+ value = route.send(key)
+ if value
+ @cumulated_stats[tag.id][:stats][key][:value] = 0 if @cumulated_stats[tag.id][:stats][key][:value].nil?
+ @cumulated_stats[tag.id][:stats][key][:value] += value
+ @cumulated_stats[tag.id][:stats][key][:min] = value if @cumulated_stats[tag.id][:stats][key][:min].nil? || value < @cumulated_stats[tag.id][:stats][key][:min]
+ @cumulated_stats[tag.id][:stats][key][:max] = value if @cumulated_stats[tag.id][:stats][key][:max].nil? || value > @cumulated_stats[tag.id][:stats][key][:max]
+ end
+ }
+ route.quantities.each { |du_id, value|
+ @cumulated_stats[tag.id][:stats][du_id][:value] = 0 if @cumulated_stats[tag.id][:stats][du_id][:value].nil?
+ @cumulated_stats[tag.id][:stats][du_id][:value] += value
+ @cumulated_stats[tag.id][:stats][du_id][:min] = value if @cumulated_stats[tag.id][:stats][du_id][:min].nil? || value < @cumulated_stats[tag.id][:stats][du_id][:min]
+ @cumulated_stats[tag.id][:stats][du_id][:max] = value if @cumulated_stats[tag.id][:stats][du_id][:max].nil? || value > @cumulated_stats[tag.id][:stats][du_id][:max]
+ }
+ }
+ @cumulated_stats[tag.id][:stats][:route_size][:value] = routes.length
+ }
+
+ respond_to do |format|
+ format.html
+ format.js { render partial: 'statistics', locals: { cumulated_stats: @cumulated_stats, current_user: current_user } }
+ end
+ end
+
def refresh
respond_to do |format|
if @planning.compute_saved
@@ -735,4 +777,22 @@ def format_csv(format)
response.headers['Content-Disposition'] = 'attachment; filename="' + filename + '.csv"'
end
end
+
+ def stat_init_hash
+ {
+ route_size: { label: t('activerecord.attributes.route.route_size'), help: t('plannings.edit.route_size_help'), icon: 'fa-truck-field', value: nil, min: nil, max: nil},
+ size_active: { label: t('activerecord.attributes.route.size_active'), help: t('plannings.edit.size_active_help'), icon: 'fa-check-square', value: nil, min: nil, max: nil},
+ emission: { label: t('activerecord.attributes.route.emission'), suffix: t('all.unit.kgco2e_html'), help: t('plannings.edit.emission_help'), icon: 'fa-flask', value: nil, min: nil, max: nil},
+ distance: { label: t('activerecord.attributes.route.distance'), help: t('plannings.edit.route_distance_help'), icon: 'fa-road', value: nil, min: nil, max: nil},
+ duration: { label: t('activerecord.attributes.route.duration'), help: t('plannings.edit.route_duration_help'), icon: 'fa-stopwatch', value: nil, min: nil, max: nil},
+ drive_time: { label: t('activerecord.attributes.route.drive_time'), help: t('plannings.edit.route_duration_help'), icon: 'fa-stopwatch', value: nil, min: nil, max: nil},
+ wait_time: { label: t('activerecord.attributes.route.wait_time'), help: t('plannings.edit.wait_time_help'), icon: 'fa-hourglass-half', value: nil, min: nil, max: nil},
+ visits_duration: { label: t('activerecord.attributes.route.visits_duration'), help: t('plannings.edit.route_visits_duration_help'), icon: 'fa-hourglass-half', value: nil, min: nil, max: nil},
+ revenue: { label: t('activerecord.attributes.route.revenue'), suffix: t("all.unit.currency_symbol.#{current_user.prefered_currency}"), help: t('plannings.edit.revenue_help'), icon: 'fa-hand-holding-dollar', value: nil, min: nil, max: nil},
+ total_cost: { label: t('activerecord.attributes.route.total_cost'), suffix: t("all.unit.currency_symbol.#{current_user.prefered_currency}"), help: t('plannings.edit.cost_help'), icon: 'fa-coins', value: nil, min: nil, max: nil},
+ balance: { label: t('activerecord.attributes.route.balance'), suffix: t("all.unit.currency_symbol.#{current_user.prefered_currency}"), help: t('plannings.edit.balance_help'), icon: 'fa-balance-scale', value: nil, min: nil, max: nil},
+ }.merge(current_user.customer.deliverable_units.map{ |du|
+ [du.id, { label: du.label&.capitalize || du.id, suffix: du.label || du.id, icon: du.icon || 'fa-boxes', value: nil, min: nil, max: nil}]
+ }.to_h)
+ end
end
diff --git a/app/helpers/plannings_helper.rb b/app/helpers/plannings_helper.rb
index fa5285ed7..d26eff818 100644
--- a/app/helpers/plannings_helper.rb
+++ b/app/helpers/plannings_helper.rb
@@ -162,4 +162,44 @@ def external_callback_converted_url(summary, current_user, route_hash = nil)
''
end
end
+
+ def planning_stat_value(key, hash, current_user)
+ content_tag(:div) do
+ html = []
+
+ html << content_tag(:div, class: 'primary route-info') do
+ concat content_tag(:i, '', class: "fa fa-fw #{hash[:icon]}", help: hash[:help])
+ concat ' '
+ concat format_stat_value(key, hash, hash[:value], current_user)
+ end
+
+ if hash[:min] && hash[:max]
+ html << content_tag(:div, class: 'info route-info') do
+ concat t('plannings.statistics.min')
+ concat ' '
+ concat format_stat_value(key, hash, hash[:min], current_user)
+ end
+
+ html << content_tag(:div, class: 'info route-info') do
+ concat t('plannings.statistics.max')
+ concat ' '
+ concat format_stat_value(key, hash, hash[:max], current_user)
+ end
+ end
+
+ safe_join(html)
+ end
+ end
+
+ private
+
+ def format_stat_value(key, hash, value, current_user)
+ if key == :distance
+ locale_distance(value, current_user.prefered_unit)
+ elsif key.is_a?(Symbol) && (key.end_with?('_time') || key.end_with?('duration'))
+ value && time_over_day(value)
+ else
+ "#{value&.round(2)} #{hash[:suffix]}".html_safe
+ end
+ end
end
diff --git a/app/models/route.rb b/app/models/route.rb
index 5485e2647..3bf3e973b 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -120,35 +120,24 @@ def work_time_value
vehicle_usage.default_work_time if vehicle_usage? && vehicle_usage.default_work_time
end
- def init_route_data
- {
- stop_distance: 0,
- stop_no_path: false,
- stop_out_of_drive_time: nil,
- stop_out_of_work_time: nil,
- out_of_max_ride_distance: nil,
- out_of_max_ride_duration: nil,
- emission: nil,
- start: nil,
- end: nil,
- distance: 0,
- drive_time: nil,
- wait_time: nil,
- visits_duration: nil,
- quantities: nil,
- revenue: nil,
- cost_distance: nil,
- cost_fixed: nil,
- cost_time: nil
- }
- end
-
def is_expired?
return false if planning.date.nil? || stops.only_active_stop_visits.empty? || stops.only_active_stop_visits.last.time.nil?
planning.date + stops.only_active_stop_visits.last.time.seconds + 12.hour < DateTime.now
end
+ def duration
+ visits_duration.to_i + wait_time.to_i + drive_time.to_i + (vehicle_usage ? vehicle_usage.default_service_time_start.to_i + vehicle_usage.default_service_time_end.to_i : 0)
+ end
+
+ def balance
+ [revenue || 0, total_cost].compact.reduce(&:-)
+ end
+
+ def total_cost
+ [cost_distance, cost_fixed, cost_time].compact.reduce(&:+)
+ end
+
def store_traces(geojson_tracks_store, trace, options = {})
if trace && !options[:no_geojson]
geojson_tracks_store << {
@@ -908,6 +897,29 @@ def assign_defaults
self.locked = false
end
+ def init_route_data
+ {
+ stop_distance: 0,
+ stop_no_path: false,
+ stop_out_of_drive_time: nil,
+ stop_out_of_work_time: nil,
+ out_of_max_ride_distance: nil,
+ out_of_max_ride_duration: nil,
+ emission: nil,
+ start: nil,
+ end: nil,
+ distance: 0,
+ drive_time: nil,
+ wait_time: nil,
+ visits_duration: nil,
+ quantities: nil,
+ revenue: nil,
+ cost_distance: nil,
+ cost_fixed: nil,
+ cost_time: nil
+ }
+ end
+
def in_optimization_context?
planning&.in_optimization_context?
end
diff --git a/app/views/plannings/_edit.html.haml b/app/views/plannings/_edit.html.haml
index ff3e06f82..d212714e7 100644
--- a/app/views/plannings/_edit.html.haml
+++ b/app/views/plannings/_edit.html.haml
@@ -165,6 +165,10 @@
%a.export_spreadsheet
%i.fa.fa-table.fa-fw
= t '.export.spreadsheet'
+ %li
+ = link_to planning_statistics_path(@planning) do
+ %i.fa.fa-chart-column.fa-fw
+ = t '.export.statistics'
%li.divider{role: "separator"}
%li
= link_to api_planning_calendar_path(@planning, api_key: current_user.api_key) do
diff --git a/app/views/plannings/_statistics.html.haml b/app/views/plannings/_statistics.html.haml
new file mode 100644
index 000000000..cead9f9df
--- /dev/null
+++ b/app/views/plannings/_statistics.html.haml
@@ -0,0 +1,12 @@
+- cumulated_stats&.each do |tag_id, data|
+ %div{class: 'tag-stats'}
+ .row
+ .col-xs-12
+ %h3.tag-info
+ = tag_icon(data[:tag])
+ = data[:label]
+ .row.route-data
+ - data[:stats].each do |key, hash|
+ .col-xs-4.col-md-3.col-lg-2
+ %b= hash[:label]
+ = planning_stat_value(key, hash, current_user)
diff --git a/app/views/plannings/_statistics.js.erb b/app/views/plannings/_statistics.js.erb
new file mode 100644
index 000000000..c4a054c3f
--- /dev/null
+++ b/app/views/plannings/_statistics.js.erb
@@ -0,0 +1,2 @@
+$('#statistics-container').html("<%= j(render partial: 'plannings/statistics.html.haml', locals: local_assigns) %>");
+var locals = <%= local_assigns.to_json.html_safe %>;
diff --git a/app/views/plannings/_tag_selector.haml b/app/views/plannings/_tag_selector.haml
new file mode 100644
index 000000000..fd0733955
--- /dev/null
+++ b/app/views/plannings/_tag_selector.haml
@@ -0,0 +1,9 @@
+.col-xs-12.multiple
+ %label
+ = t('plannings.statistics.select_tags')
+ = select_tag :planning_tag_ids,
+ options_from_collection_for_select(current_user.customer.tags, :id, :label),
+ { multiple: true,
+ class: 'form-control width_1_2 selectpicker',
+ 'data-live-search': true,
+ 'data-none-selected-text': t('plannings.statistics.select_tags_placeholder') }
diff --git a/app/views/plannings/statistics.html.haml b/app/views/plannings/statistics.html.haml
new file mode 100644
index 000000000..48dafc7ed
--- /dev/null
+++ b/app/views/plannings/statistics.html.haml
@@ -0,0 +1,15 @@
+- javascript 'planning'
+- content_for :title, t('.title')
+.container
+ %h1
+ = t '.title'
+ #route_selector.row
+ = render 'tag_selector'
+
+ #statistics-container
+ = render partial: 'statistics', locals: { cumulated_stats: @cumulated_stats, current_user: current_user }
+
+:ruby
+ controller.js(
+ planning_id: @planning.id
+ )
diff --git a/config/locales/en.yml b/config/locales/en.yml
index a5b08000a..1d39857b7 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -347,8 +347,17 @@ en:
customer_dashboard_url: Customer's Dashboard URL
planning_dashboard_url: Planning's Dashboard URL
route:
+ route_size: Routes
+ size_active: Stops
distance: Distance
emission: Emission
+ duration: Duration
+ drive_time: Drive time
+ wait_time: Wait time
+ visits_duration: Visits duration
+ revenue: Revenue
+ total_cost: Total cost
+ balance: Balance
outdated: Outdated
start: Start
end: End
@@ -1851,6 +1860,7 @@ en:
success: CSV file successfully exported
print: Print
spreadsheet: Spreadsheet CSV
+ statistics: Statistics by Categories
gpx_route: GPX Route
gpx_track: GPX Track
kmz_track: KMZ Track
@@ -1873,6 +1883,7 @@ en:
refresh: Refresh
refresh_help: 'Plan parameters have been changed, it is required to recalculate'
outdated_zoning: Sectorisation changed. Apply again to refresh.
+ size_active_help: Active stops
stops_help: Plan stops / total
duration_help: Total duration
distance_help: Total distance
@@ -1901,6 +1912,7 @@ en:
route_distance_help: Route distance
route_quantity_help: Quantity / Vehicle capacity
route_quantity_loading_help: Quantity to load before the start of the route
+ route_size_help: Number of Routes
route_speed_average_help: Vehicle's speed average
route_driving_time_help: Driving time
route_visits_duration_help: Visits duration
@@ -2178,6 +2190,12 @@ en:
default: Default
show:
print: Print
+ statistics:
+ title: Statistics
+ select_tags: Select the categories
+ select_tags_placeholder: Select the categories to display
+ min: 'min:'
+ max: 'max:'
plannings_by_destinations:
show:
affect_destinations: Move visits
@@ -2904,6 +2922,9 @@ en:
route_selected: selected route
routes_selected: selected routes
search_placeholder: Search
+ tag_none: No selected category
+ tag_selected: selected category
+ tags_selected: selected categories
placeholder: Write here
dialog:
close: Close
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index dbd39f4d6..db4c9153f 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -347,7 +347,16 @@ fr:
customer_dashboard_url: URL du tableau de bord du compte
planning_dashboard_url: URL du tableau de bord de planification
route:
+ route_size: Tournées
+ size_active: Arrêts
distance: Distance
+ duration: Durée
+ drive_time: Temps de conduite
+ wait_time: Temps d'attente
+ visits_duration: Durée des visites
+ revenue: Recettes
+ total_cost: Coût total
+ balance: Solde
emission: Émissions
outdated: Obsolète
start: Début
@@ -1935,6 +1944,7 @@ fr:
success: CSV exporté avec succès
print: Imprimer
spreadsheet: Tableur CSV
+ statistics: Statistiques par Compétences
gpx_route: Route GPX
gpx_track: Tracé GPX
kmz_track: Tracé KMZ
@@ -1959,6 +1969,7 @@ fr:
Des paramètres ou des adresses utilisés par ce plan de tournées ont été
modifiés, il est nécessaire de le recalculer
outdated_zoning: La sectorisation a été modifiée. Appliquez à nouveau pour actualiser.
+ size_active_help: Arrêts planifiés
stops_help: Arrêts planifiés / total
duration_help: Somme des durées des tournées
distance_help: Somme des distances des tournées
@@ -1991,6 +2002,7 @@ fr:
route_distance_help: Distance de la tournée
route_quantity_help: Chargement / Capacité véhicule
route_quantity_loading_help: À charger avant le début de la tournée
+ route_size_help: Nombre de Tournées
route_speed_average_help: Vitesse moyenne du véhicule
route_driving_time_help: Durée de conduite
route_visits_duration_help: Durée des visites
@@ -2278,6 +2290,12 @@ fr:
default: Défaut
show:
print: Imprimer
+ statistics:
+ title: Statistiques
+ select_tags: "Sélectionner les Compétences"
+ select_tags_placeholder: "Choisir les Compétences à afficher"
+ min: 'min :'
+ max: 'max :'
plannings_by_destinations:
show:
affect_destinations: Déplacer les visites
@@ -3033,6 +3051,9 @@ fr:
route_none: Aucune tournée sélectionnée
route_selected: tournées sélectionnée
routes_selected: tournées sélectionnées
+ tag_none: Aucune compétence sélectionnée
+ tag_selected: compétences sélectionnées
+ tags_selected: compétences sélectionnées
search_placeholder: Rechercher
placeholder: Écrire ici
dialog:
diff --git a/config/routes.rb b/config/routes.rb
index 37da5bd51..e16ed6f5f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -133,6 +133,7 @@
get 'modal'
patch 'switch'
patch 'duplicate'
+ get 'statistics'
patch ':route_id/active/:active' => 'plannings#active'
patch ':route_id/reverse_order' => 'plannings#reverse_order'
patch ':route_id/:stop_id' => 'plannings#update_stop'