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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/rubyonrails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions app/assets/javascripts/plannings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -2845,5 +2894,8 @@ Paloma.controller('Plannings', {
},
show: function() {
plannings_show(this.params);
},
statistics: function() {
planning_statistics(this.params);
}
});
9 changes: 5 additions & 4 deletions app/assets/javascripts/scaffolds.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() || [];

Expand All @@ -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(
'<li class="select2-selection__choice"><span class="select2-selection__choice__display">' + text + '<span></li>'
);
Expand Down
11 changes: 11 additions & 0 deletions app/assets/stylesheets/plannings.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions app/assets/stylesheets/scaffolds.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
62 changes: 61 additions & 1 deletion app/controllers/plannings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
40 changes: 40 additions & 0 deletions app/helpers/plannings_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading