-
+
@@ -12,12 +12,12 @@
-
-
+
diff --git a/src/fragments/forms/map-form/components/isochrones/isochrones.js b/src/fragments/forms/map-form/components/isochrones/isochrones.js
index a29f7c018..0f19be3bc 100644
--- a/src/fragments/forms/map-form/components/isochrones/isochrones.js
+++ b/src/fragments/forms/map-form/components/isochrones/isochrones.js
@@ -1,8 +1,10 @@
-import FormActions from '@/fragments/forms/map-form/components/form-actions/FormActions'
+import MapFormMixin from '../map-form-mixin'
import MapViewDataBuilder from '@/support/map-data-services/map-view-data-builder'
import FieldsContainer from '@/fragments/forms/fields-container/FieldsContainer'
import OrsFilterUtil from '@/support/map-data-services/ors-filter-util'
+import FormActions from '@/fragments/forms/map-form/components/form-actions/FormActions'
import PlaceInput from '@/fragments/forms/place-input/PlaceInput.vue'
+import {EventBus} from '@/common/event-bus'
import {Isochrones} from '@/support/ors-api-runner'
import AppMode from '@/support/app-modes/app-mode'
import MapViewData from '@/models/map-view-data'
@@ -10,25 +12,24 @@ import constants from '@/resources/constants'
import appConfig from '@/config/app-config'
import Draggable from 'vuedraggable'
import Place from '@/models/place'
-import {EventBus} from '@/common/event-bus'
// Local components
import IschronesDetails from './components/isochrones-details/IsochronesDetails'
-import MapFormMixin from '../map-form-mixin'
export default {
mixins: [MapFormMixin],
data: () => ({
+ active: true,
mode: constants.modes.isochrones,
mapViewData: new MapViewData(),
places: [new Place()],
roundTripActive: false
}),
components: {
- PlaceInput,
FieldsContainer,
- Draggable,
FormActions,
+ PlaceInput,
+ Draggable,
IschronesDetails
},
computed: {
@@ -121,6 +122,9 @@ export default {
} else {
this.places = [new Place()]
}
+ },
+ '$store.getters.mode': function (activeMode) {
+ this.active = activeMode === this.mode
}
},
methods: {
diff --git a/src/fragments/forms/map-form/components/map-form-mixin.js b/src/fragments/forms/map-form/components/map-form-mixin.js
index 147dc8f9f..85f580795 100644
--- a/src/fragments/forms/map-form/components/map-form-mixin.js
+++ b/src/fragments/forms/map-form/components/map-form-mixin.js
@@ -8,17 +8,6 @@ import Place from '@/models/place'
import {EventBus} from '@/common/event-bus'
export default {
- props: {
- active: {
- default: true,
- type: Boolean
- }
- },
- watch: {
- active: function (newVal) {
- this.activeChanged(newVal)
- }
- },
computed: {
/**
* Provides an accessor to the ors map filters object
@@ -44,16 +33,6 @@ export default {
}
},
methods: {
- /**
- * If the active state changes
- * reset the places array
- * @param {Boolean} isActive
- */
- activeChanged(isActive) {
- if (isActive) {
- // this.places = [new Place()]
- }
- },
/**
* Toggle round trip
*/
diff --git a/src/fragments/forms/map-form/components/optimization/Optimization.vue b/src/fragments/forms/map-form/components/optimization/Optimization.vue
new file mode 100644
index 000000000..a2ae83cc4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/Optimization.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+ {{ $t('optimization.jobs') }} (Max: 50)
+
+
+
+ edit
+
+
+ {{ $t('optimization.manage') + $t('optimization.jobs') }}
+
+
+ keyboard_arrow_up
+ keyboard_arrow_down
+
+
+
+
+ map
+ {{ $t('optimization.addFromMap') + $t('optimization.job') }}
+
+
+
+
+
+
+ work
+ {{ $t('optimization.savedJobs') + jobs.length }}
+
+
+
+
+ {{ $t('optimization.vehicles') }} (Max: 3)
+
+
+
+ edit
+
+
+ {{ $t('optimization.manage') + $t('optimization.vehicles') }}
+
+
+
+
+ map
+ {{ $t('optimization.addFromMap') + $t('optimization.vehicle') }}
+
+
+
+
+
+
+
+
+ edit
+
+
{{$t('optimization.manage') + $t('optimization.skills')}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/EditDialog.cy.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/EditDialog.cy.js
new file mode 100644
index 000000000..e76b0fd01
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/EditDialog.cy.js
@@ -0,0 +1,59 @@
+import I18nBuilder from '@/i18n/i18n-builder'
+import EditDialog from './EditDialog.vue'
+import store from '@/store/store'
+import Job from '@/models/job'
+import Skill from '@/models/skill'
+import Vehicle from '@/models/vehicle'
+
+const i18n = I18nBuilder.build()
+
+describe('
', () => {
+ const skills= [Skill.fromObject({id: 1, name: 'heating'})]
+ const jobs = [
+ Job.fromObject({
+ id: 1,
+ location: [8.690314292907717,49.4144633204352],
+ service: 2400,
+ delivery: [2],
+ pickup: [3]
+ })]
+ const vehicles = [
+ Vehicle.fromObject({
+ id: 1,
+ start: [8.682546615600588,49.41242512006711],
+ end: [8.682546615600588,49.41242512006711],
+ profile: 'driving-car',
+ capacity:[5]
+ }),
+ Vehicle.fromObject({
+ id: 2,
+ start: [8.667955398559572,49.41915365183029],
+ end: [8.68597984313965,49.41281601436811],
+ profile: 'driving-car',
+ capacity: [10]
+ })
+ ]
+ it('renders', () => {
+ cy.mount(EditDialog, {
+ propsData: {
+ data: [], skills: [], editProp: 'jobs'
+ }, i18n: i18n, store: store
+ })
+ })
+ it('renders with jobs', () => {
+ cy.mount(EditDialog, {
+ propsData: {
+ data: jobs, skills: skills, editProp: 'jobs'
+ }, i18n: i18n, store: store
+ })
+ cy.get('[data-cy="dataCards"]').should('have.length', 1)
+ })
+ it('renders with vehicles', () => {
+ cy.mount(EditDialog, {
+ propsData: {
+ data: vehicles, skills: skills, editProp: 'vehicles'
+ }, i18n: i18n, store: store
+ })
+ cy.get('[data-cy="dataCards"]').should('have.length', 2)
+ })
+})
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/EditDialog.vue b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/EditDialog.vue
new file mode 100644
index 000000000..9a62a3046
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/EditDialog.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+ {{ headerText }}
+
+
+ map
+ {{ content.fromMap }}
+
+
+ {{ content.maxWarning }}
+
+
+
+ Job {{ d.id }} - {{ d.location ? d.location[0].toPrecision(8) + ',' + d.location[1].toPrecision(8) : 'please add Location'}}
+ {{vehicleIcon(d.profile)}}{{ content.item }} {{ d.id }}
+
+ check
+
+
+ edit
+
+
+ content_copy
+
+
+ delete
+
+
+
+
+
+ {{ $t(`optimization.${k}`) }}: {{humanisedTime(v)}}
+ {{ $t(`optimization.${k}`) }}: {{v[0]}}
+ {{ $t(`optimization.${k}`) }}: {{skillNames(d)}}
+
+
+
+ Start: {{ d.start[0].toPrecision(8) }}, {{ d.start[1].toPrecision(8) }} - End: {{ d.end[0].toPrecision(8) }}, {{ d.end[1].toPrecision(8) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings
+ {{ 'manage Skills' }}
+
+
+
+
+
+
+
+
+ {{ $t('optimization.clear') + content.expected }}
+
+
+
+ {{$t('global.cancel')}}
+
+
+
+ {{$t('global.save')}}
+ save
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/edit-dialog.css b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/edit-dialog.css
new file mode 100644
index 000000000..a21a31880
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/edit-dialog.css
@@ -0,0 +1,13 @@
+.edit-header-btn {
+ padding: 0 20px;
+ min-width: 5px;
+ float: right;
+ margin: 0;
+ height: 24px;
+ background: white;
+}
+
+.edit-btn {
+ float: right;
+ margin-left: 30px;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/edit-dialog.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/edit-dialog.js
new file mode 100644
index 000000000..2a4649cdc
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/edit-dialog.js
@@ -0,0 +1,365 @@
+import RouteImporter from '@/fragments/forms/route-importer/RouteImporter.vue'
+import Download from '@/fragments/forms/map-form/components/download/Download.vue'
+import MapFormBtn from '@/fragments/forms/map-form-btn/MapFormBtn.vue'
+import OrsFilterUtil, {vehicleIcon} from '@/support/map-data-services/ors-filter-util'
+import constants from '@/resources/constants'
+import PlaceAutocomplete from '@/fragments/forms/place-input/PlaceAutocomplete.vue'
+import ProfileSelectorOption from '@/fragments/forms/profile-selector/components/profile-selector-option/ProfileSelectorOption'
+import EditSkills from '@/fragments/forms/map-form/components/optimization/components/edit-skills/EditSkills.vue'
+import OptimizationImport from '@/fragments/forms/map-form/components/optimization/components/optimization-import/OptimizationImport.vue'
+import {EventBus} from '@/common/event-bus'
+import {integer} from 'vee-validate/dist/rules.esm'
+import Job from '@/models/job'
+import Vehicle from '@/models/vehicle'
+import Skill from '@/models/skill'
+import {vehicleColors} from '@/support/optimization-utils'
+import geoUtils from '@/support/geo-utils'
+
+export default {
+ data: () => ({
+ isEditOpen: true,
+ editId: 0,
+ editData: [],
+ dataCopy: '',
+ editSkills: [],
+ skillsCopy: '',
+ jobsBox: false,
+ vehiclesBox: false,
+ pickPlaceSupported: true,
+ focused: false,
+ searching: false,
+ debounceTimeoutId: null,
+ showSkillManagement: false,
+ isSkillsOpen: false,
+ isImportOpen: false,
+ onlyStartPoint: false,
+ newEndPoint: false,
+ activeProfileSlug: null,
+ activeVehicleType: null,
+ }),
+ props: {
+ data: {
+ Type: Array,
+ Required: true
+ },
+ skills: {
+ Type: Array[Skill],
+ Required: true
+ },
+ editProp: {
+ Type: String,
+ Required: true
+ },
+ index: {
+ Type: integer,
+ Required: false
+ },
+ disabledActions: {
+ default: () => [],
+ type: Array,
+ }
+ },
+ components: {
+ RouteImporter,
+ Download,
+ MapFormBtn,
+ PlaceAutocomplete,
+ ProfileSelectorOption,
+ EditSkills,
+ OptimizationImport,
+ EventBus
+ },
+ computed: {
+ content () {
+ let item
+ let items
+ let itemClass
+ let maxLength
+ let skillsMessage
+
+ if(this.jobsBox){
+ item = 'job'
+ itemClass = Job
+ maxLength = 50
+ skillsMessage = 'Skills needed for this job'
+ } else if(this.vehiclesBox){
+ item = 'vehicle'
+ itemClass = Vehicle
+ maxLength = 3
+ skillsMessage = 'Skills this vehicle has'
+ }
+ items = item + 's'
+ return {
+ item: item.charAt(0).toUpperCase() + item.slice(1),
+ class: itemClass,
+ maxLength: maxLength,
+ maxWarning: this.$t('optimization.maxWarning') + maxLength + this.$t(`optimization.${items}`),
+ import: this.$t('optimization.import') + this.$t(`optimization.${items}`),
+ edit: this.$t('optimization.edit') + this.$t(`optimization.${item}`),
+ add: this.$t('optimization.add') + this.$t(`optimization.${item}`),
+ duplicate: this.$t('optimization.duplicate') + this.$t(`optimization.${item}`),
+ remove: this.$t('optimization.remove') + this.$t(`optimization.${item}`),
+ header: this.$t('optimization.manage') + this.$t(`optimization.${items}`),
+ fromMap: this.$t('optimization.addFromMap') + this.$t(`optimization.${item}`),
+ expected: items,
+ changedEvent: `${items}Changed`,
+ skills: skillsMessage,
+ emptyLoc: `Added ${item} does not have a valid location`,
+ }
+ },
+ headerText () {
+ let editing = ''
+ if (this.editId !== 0) {
+ editing = ' - editing ' + this.editId
+ }
+ return this.content.header + editing
+ },
+ // returns true if start and end point are the same
+ sameStartEndPoint () {
+ const id = this.editId - 1
+ return this.editData[id].start[0] === this.editData[id].end[0] && this.editData[id].start[1] === this.editData[id].end[1]
+ },
+ // check if one of the items does not have a location
+ hasEmptyLocation () {
+ for (let item of this.editData) {
+ if (item.location === null || item.start === null) {
+ return true
+ }
+ }
+ return false
+ },
+ profilesMapping () {
+ const filter = OrsFilterUtil.getFilterRefByName(constants.profileFilterName)
+ return filter.mapping
+ },
+ editSkillsJson () {
+ const jsonSkills = []
+ for (const skill of this.editSkills) {
+ jsonSkills.push(skill.toJSON())
+ }
+ return jsonSkills
+ },
+ editDataJson () {
+ const jsonData = []
+ for (const data of this.editData) {
+ jsonData.push(data.toJSON())
+ }
+ return jsonData
+ },
+ },
+ created () {
+ if (this.editProp === 'jobs') {
+ this.jobsBox = true
+ this.dataCopy = localStorage.getItem('jobs')
+ } else if (this.editProp === 'vehicles') {
+ this.vehiclesBox = true
+ this.dataCopy = localStorage.getItem('vehicles')
+ }
+
+ for (let item of this.data) {
+ this.editData.push(item.clone())
+ }
+ for (const skill of this.skills) {
+ this.editSkills.push(skill.clone())
+ }
+
+ if (this.index > 0) {
+ this.editId = this.index
+ }
+
+ // close editJobs box to pick a place from the map
+ EventBus.$on('pickAPlace', () => {
+ this.closeEditModal()
+ })
+ },
+ methods: {
+ vehicleColors,
+ vehicleIcon,
+ // close editJobs dialog
+ closeEditModal () {
+ this.isEditOpen = false
+ this.$emit('close')
+ },
+
+ contentUploaded (data) {
+ this.$emit('contentUploaded', data)
+ },
+
+ humanisedTime (time) {
+ const data = geoUtils.getHumanizedTimeAndDistance({duration: time}, this.$t('global.units'))
+ return data.duration
+ },
+
+ clearData () {
+ this.editId = 0
+ this.editData = []
+ },
+
+ // TODO: add a recover option?
+
+ // save jobs from JSON
+ saveImport(data) {
+ if (this.jobsBox) {
+ this.editData = data.jobs
+ } else if (this.vehiclesBox) {
+ this.editData = data.vehicles
+ }
+
+ let importedSkill = []
+ for (const d of this.editData) {
+ importedSkill.push(...d.skills)
+ }
+ let newSkillIds = []
+ for (const s of importedSkill) {
+ if (!newSkillIds.includes(s.id)) {
+ newSkillIds.push(s.id)
+ }
+ }
+ let editSkillIds = []
+ for (const s of this.editSkills) {
+ editSkillIds.push(s.id)
+ }
+ newSkillIds.sort((a,b) => a-b)
+ for (const id of newSkillIds) {
+ if (!editSkillIds.includes(id)) {
+ this.editSkills.push(new Skill(' Skill from imported ' + this.content.item + ' ' + id, id))
+ }
+ }
+ this.$emit('skillsChanged', this.editSkills)
+ localStorage.setItem('skills', JSON.stringify(this.editSkillsJson))
+ this.saveItems()
+ this.isImportOpen = false
+ },
+
+ // check if vehicle has only a time window start and no time window end
+ validateTimeWindow () {
+ for (let index in this.editData) {
+ if (this.editData[index].time_window[0] === '') {
+ this.editData[index].time_window = []
+ } else if (this.editData[index].time_window.length === 1 || this.editData[index].time_window[1] === '') {
+ this.editData[index].time_window[1] = this.editData[index].time_window[0] + 3600
+ }
+ }
+ },
+ // save items and close the edit box, show error if one or more of them has an empty location
+ saveItems () {
+ if (this.content.item === 'Vehicle') {
+ this.validateTimeWindow()
+ }
+ if (this.hasEmptyLocation) {
+ this.showError(this.content.emptyLoc, {timeout: 3000})
+ } else {
+ if(JSON.stringify(this.editDataJson) !== this.dataCopy){
+ this.$emit(this.content.changedEvent, this.editData)
+ }
+ this.closeEditModal()
+ }
+ },
+ // add an item
+ // if not chosen from map it has an empty location and placeAutocomplete is activated
+ addItem (fromMap) {
+ if(fromMap) {
+ this.showInfo(this.$t('placeInput.clickOnTheMapToSelectAPlace'))
+ this.closeEditModal()
+ this.setPickPlaceSource()
+ } else {
+ let item
+ if (this.jobsBox) {
+ item = new Job()
+ } else if (this.vehiclesBox) {
+ item = new Vehicle()
+ }
+ const id = this.editData.length + 1
+ item.setId(id)
+ this.editData.push(item)
+ this.editId = id
+ }
+ },
+ // set the pick place input source
+ setPickPlaceSource () {
+ if (this.pickPlaceSupported) {
+ this.$store.commit('pickPlaceIndex', this.editData.length)
+ this.$store.commit('pickPlaceId', this.editData.length + 1)
+ if (this.jobsBox) {
+ this.$store.commit('pickEditSource', 'jobs')
+ } else if (this.vehiclesBox) {
+ this.$store.commit('pickEditSource', 'vehicleStart')
+ }
+ }
+ },
+ // delete old location when switching to search to activate placeAutocomplete
+ switchToSearch (mode) {
+ if (this.jobsBox) {
+ this.editData[this.editId - 1].location = null
+ } else if (this.vehiclesBox) {
+ if (mode === 'start') {
+ if (!this.sameStartEndPoint) {
+ this.onlyStartPoint = true
+ }
+ this.editData[this.editId - 1].start = null
+ } else if (mode === 'end') {
+ this.newEndPoint = true
+ this.editData[this.editId - 1].end = null
+ }
+ }
+ },
+ removeItem (id) {
+ this.editData.splice(id-1,1)
+ for (const i in this.editData) {
+ this.editData[i].setId(parseInt(i)+1)
+ }
+ },
+ removeEndPoint (index) {
+ this.editData[index].end = this.editData[index].start
+ },
+ duplicateItem (index) {
+ let newItem = this.editData[index-1].clone()
+ let id = this.editData.length + 1
+ newItem.setId(id)
+ this.editData.push(newItem)
+ this.editId = id
+ },
+ // update job skills selection when the skills were changed
+ skillsChanged(editedSkills) {
+ let newSkills = []
+ for (const skill of editedSkills) {
+ newSkills.push(skill.clone())
+ }
+ this.editSkills = newSkills
+ this.$emit('skillsChanged', this.editSkills)
+ },
+ skillNames(item) {
+ let names = ''
+ for (const skill of item.skills) {
+ if(names === ''){
+ names = skill.name
+ } else {
+ names = names + ', ' + skill.name
+ }
+ }
+ return names
+ },
+
+ // when profile selected in selector, update vehicle properties
+ profileSelected (data) {
+ this.activeProfileSlug = data.profileSlug
+ this.activeVehicleType = data.vehicleTypeSlug
+
+ OrsFilterUtil.setFilterValue(constants.profileFilterName, data.profileSlug)
+
+ this.editData[this.editId - 1].profile = data.profileSlug
+ },
+ // return currently active vehicle profile property
+ vehicleProfile(id) {
+ const vt = this.editData[id].profile
+ let profile
+ if (vt !== 'wheelchair') {
+ const profilePart = vt.split('-')[0]
+ profile = profilePart.concat('-*')
+ } else {
+ profile = vt
+ }
+ return profile
+ },
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.cs-cz.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.cs-cz.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.cs-cz.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.de-de.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.de-de.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.de-de.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.en-us.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.en-us.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.en-us.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.es-es.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.es-es.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.es-es.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.fr-fr.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.fr-fr.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.fr-fr.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.hu-hu.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.hu-hu.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.hu-hu.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.it-it.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.it-it.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.it-it.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.pt-br.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.pt-br.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.pt-br.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.ro-ro.js b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.ro-ro.js
new file mode 100644
index 000000000..4ff8b13ba
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-dialog/i18n/edit-dialog.i18n.ro-ro.js
@@ -0,0 +1,9 @@
+export default {
+ editDialog: {
+ keepEdits: 'Keep edits',
+ service: 'Service time (in seconds)',
+ timeWindow: ' of time window (in seconds passed since 00:00 or timestamp)',
+ start: 'Start',
+ end: 'End'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/EditSkills.vue b/src/fragments/forms/map-form/components/optimization/components/edit-skills/EditSkills.vue
new file mode 100644
index 000000000..3d4a97a8a
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/EditSkills.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ cloud_upload
+
+
+ add
+
+ {{ $t('optimization.manage') + $t('optimization.skills') }}
+
+
+
+ map
+ {{ $t('optimization.add') + $t('optimization.skill') }}
+
+
+
+
+ Skill: {{ skill.name }}
+
+ check
+
+
+ edit
+
+
+ delete
+
+ {{ $t('editSkills.inUseShort') }}
+
+
+
+
+
+ {{ $t('optimization.clear') + $t('optimization.skills') }}
+
+
+
+ {{$t('global.cancel')}}
+
+
+
+ {{$t('global.save')}}
+ save
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/edit-skills.css b/src/fragments/forms/map-form/components/optimization/components/edit-skills/edit-skills.css
new file mode 100644
index 000000000..85ef19651
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/edit-skills.css
@@ -0,0 +1,17 @@
+.edit-skills-btn {
+ padding: 0 20px;
+ min-width: 0;
+ float: right;
+ margin: 0;
+ height: 24px;
+ background: white;
+}
+
+.remove-btn {
+ float: right;
+}
+
+.edit-btn {
+ float: right;
+ margin-left: 30px;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/edit-skills.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/edit-skills.js
new file mode 100644
index 000000000..045a6d52c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/edit-skills.js
@@ -0,0 +1,108 @@
+import OptimizationImport from '@/fragments/forms/map-form/components/optimization/components/optimization-import/OptimizationImport.vue'
+import {EventBus} from '@/common/event-bus'
+import Skill from '@/models/skill'
+import Download from '@/fragments/forms/map-form/components/download/Download'
+
+export default {
+ data: () => ({
+ isSkillsOpen: true,
+ editId: 0,
+ editSkills: [],
+ selectedSkills: [],
+ pastedSkills: [],
+ JsonPlaceholder: '[{"name":"example skill","id":1}]',
+ isImportOpen: false
+ }),
+ props: {
+ skills: {
+ Type: Array[Skill],
+ Required: true
+ },
+ skillsInUse: {
+ Type: Array,
+ Required: true
+ }
+ },
+ components: {
+ OptimizationImport,
+ EventBus,
+ Download
+ },
+ computed: {
+ editSkillsJson () {
+ const jsonSkills = []
+ for (const skill of this.editSkills) {
+ jsonSkills.push(skill.toJSON())
+ }
+ return jsonSkills
+ }
+ },
+ created() {
+ for (const skill of this.skills) {
+ this.editSkills.push(skill.clone())
+ }
+
+ const context = this
+ // edit Skills box is open
+ EventBus.$on('showSkillsModal', (editId) => {
+ context.isSkillsOpen = true
+ context.editId = editId
+ })
+ },
+ methods: {
+ // close editSkills dialog
+ closeSkillsModal() {
+ this.isSkillsOpen = false
+ this.$emit('close')
+ },
+
+ clearData () {
+ let wholeSkillsInUse = []
+ let notInUse = []
+ for (const skill of this.editSkills) {
+ if (this.skillsInUse.includes(skill.id)) {
+ wholeSkillsInUse.push(skill.clone())
+ } else {
+ notInUse.push(skill.id)
+ }
+ }
+ if (notInUse.length === 0) {
+ this.showWarning(this.$t('editSkills.allSkills') + this.$t('editSkills.inUseShort'))
+ } else {
+ this.showInfo(this.$t('editSkills.onlyDelete') + this.$t('editSkills.inUseShort'))
+ this.editId = 0
+ this.editSkills = wholeSkillsInUse
+ }
+ },
+
+ // save skills from JSON
+ saveSkillImport(data) {
+ this.editSkills = data.skills
+ this.saveSkills()
+ this.isImportOpen = false
+ },
+
+ // save changed skills and emit event to update in component
+ saveSkills () {
+ this.$emit('skillsChanged', this.editSkills)
+ localStorage.setItem('skills', JSON.stringify(this.editSkillsJson))
+ this.closeSkillsModal()
+ },
+ // add a new skill
+ addSkill () {
+ const newSkill = new Skill('', this.editSkills.length + 1)
+ this.editSkills.push(newSkill)
+ },
+ // delete a skill
+ removeSkill (id) {
+ if (this.skillsInUse.includes(this.editSkills[id-1].id)) {
+ this.showWarning(this.$t('editSkills.inUse'))
+ } else {
+ this.editSkills.splice(id-1,1)
+ for (const i in this.editSkills) {
+ this.editSkills[i].setId(parseInt(i)+1)
+ }
+ }
+ },
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.cs-cz.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.cs-cz.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.cs-cz.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.de-de.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.de-de.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.de-de.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.en-us.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.en-us.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.en-us.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.es-es.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.es-es.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.es-es.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.fr-fr.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.fr-fr.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.fr-fr.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.hu-hu.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.hu-hu.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.hu-hu.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.it-it.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.it-it.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.it-it.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.pt-br.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.pt-br.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.pt-br.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.ro-ro.js b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.ro-ro.js
new file mode 100644
index 000000000..85a20fab4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/edit-skills/i18n/edit-skills.i18n.ro-ro.js
@@ -0,0 +1,8 @@
+export default {
+ editSkills: {
+ inUse: 'Skill is used in a Job or Vehicle',
+ allSkills: 'All skills are ',
+ onlyDelete: 'Only deletes skills not ',
+ inUseShort: 'in use'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.cs-cz.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.cs-cz.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.cs-cz.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.de-de.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.de-de.js
new file mode 100755
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.de-de.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.en-us.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.en-us.js
new file mode 100755
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.en-us.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.es-es.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.es-es.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.es-es.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.fr-fr.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.fr-fr.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.fr-fr.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.hu-hu.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.hu-hu.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.hu-hu.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.it-it.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.it-it.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.it-it.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.pt-br.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.pt-br.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.pt-br.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.ro-ro.js b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.ro-ro.js
new file mode 100644
index 000000000..065f51d3f
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/i18n/optimization.i18n.ro-ro.js
@@ -0,0 +1,33 @@
+
+export default {
+ optimization: {
+ optimization: 'optimization',
+ optimizationResultReady: 'Optimization result ready',
+ notPossible: 'Optimization was not possible',
+ couldNotResolveTheLocation: 'Could not resolve the location of the ',
+ maxWarning: 'The live API can consider a maximum of ',
+ genericErrorMsg: 'It was not possible to optimize Jobs',
+ savedJobs: 'Saved Jobs: ',
+ service: 'Service time',
+ capacity: 'Capacity',
+ delivery: 'Deliveries',
+ pickup: 'Pickups',
+ time_window: 'Time window',
+ optimize: 'Optimize ',
+ import: 'Import ',
+ manage: 'Manage ',
+ edit: 'Edit ',
+ add: 'Add ',
+ addFromMap: 'From map, add ',
+ remove: 'Remove ',
+ clear: 'Clear all ',
+ duplicate: 'Duplicate ',
+ nothingToManage: ' not available. Import or use button to add from map',
+ skills: 'Skills',
+ skill: 'Skill',
+ jobs: 'Jobs',
+ job: 'Job',
+ vehicles: 'Vehicles',
+ vehicle: 'Vehicle',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/item-list/JobList.vue b/src/fragments/forms/map-form/components/optimization/components/item-list/JobList.vue
new file mode 100644
index 000000000..960e8fa78
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/item-list/JobList.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/item-list/VehicleList.vue b/src/fragments/forms/map-form/components/optimization/components/item-list/VehicleList.vue
new file mode 100644
index 000000000..ec66a357d
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/item-list/VehicleList.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/item-list/item-list.css b/src/fragments/forms/map-form/components/optimization/components/item-list/item-list.css
new file mode 100644
index 000000000..0036eb3b4
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/item-list/item-list.css
@@ -0,0 +1,12 @@
+.warning-icon {
+ padding: 3px 3px 3px 3px;
+ float: right;
+ margin: 2px 15px 0 0;
+ border-radius: 15px;
+ background-color: #ffd54f;
+ color: darkgoldenrod
+}
+
+.no-shadow {
+ box-shadow: none;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/item-list/job-list.js b/src/fragments/forms/map-form/components/optimization/components/item-list/job-list.js
new file mode 100644
index 000000000..a259c6505
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/item-list/job-list.js
@@ -0,0 +1,53 @@
+import geoUtils from '@/support/geo-utils'
+import MapViewData from '@/models/map-view-data'
+
+export default {
+ data: () => ({
+ localMapViewData: null,
+ }),
+ props: {
+ jobs: {
+ Type: Array,
+ Required: true
+ },
+ mapViewData: {
+ Type: MapViewData,
+ Required: false
+ }
+ },
+ computed: {
+ unassignedIds () {
+ let unassignedIds = []
+ for (const job of this.localMapViewData.rawData.unassigned) {
+ unassignedIds.push(job.id)
+ }
+ return unassignedIds
+ }
+ },
+ watch: {
+ // Every time the response data changes the map builder is reset and the map data is reloaded
+ mapViewData: {
+ handler: function () {
+ this.localMapViewData = this.mapViewData.clone()
+ },
+ deep: true
+ },
+ },
+ methods: {
+ skillIds(job) {
+ let ids = ''
+ for (const skill of job.skills) {
+ if(ids === ''){
+ ids = skill.id
+ } else {
+ ids = ids + ', ' + skill.id
+ }
+ }
+ return ids
+ },
+ humanisedTime (time) {
+ const data = geoUtils.getHumanizedTimeAndDistance({duration: time}, this.$t('global.units'))
+ return data.duration
+ },
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/item-list/vehicle-list.js b/src/fragments/forms/map-form/components/optimization/components/item-list/vehicle-list.js
new file mode 100644
index 000000000..83999dbd2
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/item-list/vehicle-list.js
@@ -0,0 +1,42 @@
+import {vehicleIcon} from '@/support/map-data-services/ors-filter-util'
+import {vehicleColors} from '@/support/optimization-utils'
+import geoUtils from '@/support/geo-utils'
+
+export default {
+ data: () => ({
+ vehicleExtended: [true]
+ }),
+ props: {
+ vehicles: {
+ Type: Array,
+ Required: true
+ }
+ },
+ methods: {
+ vehicleColors,
+ vehicleIcon,
+ skillIds(vehicle) {
+ let ids = ''
+ for (const skill of vehicle.skills) {
+ if(ids === ''){
+ ids = skill.id
+ } else {
+ ids = ids + ', ' + skill.id
+ }
+ }
+ return ids
+ },
+ /**
+ * Returns readable time window 'start - end' where start and end is either a time or timestamp
+ * @param {Array
} prop
+ * @returns {string}
+ */
+ timeWindow (prop) {
+ const startTime = prop[0]
+ const endTime = prop[1]
+ const startHumanized = geoUtils.getHumanizedTime(startTime)
+ const endHumanized = geoUtils.getHumanizedTime(endTime)
+ return `${startHumanized} - ${endHumanized}`
+ },
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/OptimizationDetails.vue b/src/fragments/forms/map-form/components/optimization/components/optimization-details/OptimizationDetails.vue
new file mode 100644
index 000000000..765b25245
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/OptimizationDetails.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
{{$t('optimizationDetails.optimizationDetails')}}
+
+
+ {{$t('optimizationDetails.warningUnassigned')}} {{ unassignedJobsString }}
+
+
+
+
+
{{ getVehicleIconName(route.vehicle) }}{{$t('routeDetails.route')}} {{routeIndex + 1}} (Vehicle {{route.vehicle}})
+
+ directions
+
+
+
+
+
+ {{ $t(`optimization.${prop}`) }}: {{ route[prop][0] }}
+ {{ $t(`optimizationDetails.${prop}`) }}: {{ route[prop] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('optimizationDetails.schedule')}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/OptimizationSteps.vue b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/OptimizationSteps.vue
new file mode 100644
index 000000000..735a3901e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/OptimizationSteps.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+ {{typeSymbol(step.type)}} {{step.type}} {{step.id}}
+
+
+
{{$t('global.distance')}}: {{step.distance}}
+
access_time {{step.duration}}
+
{{$t('optimizationSteps.service')}}: {{step.service}}
+
{{$t('optimizationSteps.load')}} {{$t('optimizationSteps.after')}} {{step.load[0]}}
+
{{$t('optimizationSteps.waiting_time')}}: {{step.waiting_time}}
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/i18n/optimization-steps.i18n.de-de.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/i18n/optimization-steps.i18n.de-de.js
new file mode 100755
index 000000000..366414353
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/i18n/optimization-steps.i18n.de-de.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationSteps: {
+ load: 'Load ',
+ after: '(after)',
+ service: 'Service time',
+ waiting_time: 'Waiting time'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/i18n/optimization-steps.i18n.en-us.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/i18n/optimization-steps.i18n.en-us.js
new file mode 100755
index 000000000..2c75b0c8e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/i18n/optimization-steps.i18n.en-us.js
@@ -0,0 +1,9 @@
+
+export default {
+ optimizationSteps: {
+ load: 'Load ',
+ after: '(after)',
+ service: 'Service time',
+ waiting_time: 'Waiting time'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/optimization-steps.css b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/optimization-steps.css
new file mode 100644
index 000000000..6c6e63c6b
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/optimization-steps.css
@@ -0,0 +1,16 @@
+.step {
+ border-bottom: 1px solid #f0f1f4;
+ margin-bottom: 15px;
+ padding-bottom: 5px;
+ padding-left: 10px;
+}
+
+.step:last-child {
+ padding-bottom: 0;
+ margin-bottom: 0;
+}
+
+.instruction {
+ font-size: 16px;
+ margin-bottom: 8px;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/optimization-steps.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/optimization-steps.js
new file mode 100644
index 000000000..8a7c20b78
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/components/optimization-steps/optimization-steps.js
@@ -0,0 +1,40 @@
+export default {
+ name: 'OptimizationSteps',
+ props: {
+ steps: {
+ Type: Array,
+ Required: true
+ }
+ },
+ data: () => ({
+
+ }),
+ methods: {
+ typeSymbol (typeCode) {
+ let symbol = ''
+ switch (typeCode) {
+ case 'start':
+ symbol = 'place'
+ break
+ case 'job':
+ symbol = 'work'
+ break
+ case 'delivery':
+ symbol = 'local_shipping'
+ break
+ case 'pickup':
+ symbol = 'input'
+ break
+ case 'break':
+ symbol = 'snooze'
+ break
+ case 'end':
+ symbol = 'flag'
+ break
+ default:
+
+ }
+ return symbol
+ }
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.cs-cz.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.cs-cz.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.cs-cz.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.de-de.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.de-de.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.de-de.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.en-us.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.en-us.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.en-us.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.es-es.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.es-es.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.es-es.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.fr-fr.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.fr-fr.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.fr-fr.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.hu-hu.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.hu-hu.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.hu-hu.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.it-it.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.it-it.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.it-it.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.pt-br.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.pt-br.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.pt-br.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.ro-ro.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.ro-ro.js
new file mode 100644
index 000000000..d2de8f02e
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/i18n/optimization-details.i18n.ro-ro.js
@@ -0,0 +1,14 @@
+export default {
+ optimizationDetails: {
+ optimizationDetails: 'Optimized routes',
+ schedule: 'Schedule',
+ warningUnassigned: 'Warning! These jobs are unassigned: ',
+ 'distance': 'Distance',
+ 'duration': 'Duration',
+ 'service': 'Service time',
+ 'delivery': 'Deliveries',
+ 'pickup': 'Pickups',
+ 'waiting_time': 'Waiting time',
+ getInstructions: 'Get instructions for this Vehicle'
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/optimization-details.css b/src/fragments/forms/map-form/components/optimization/components/optimization-details/optimization-details.css
new file mode 100644
index 000000000..a3a8a1e49
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/optimization-details.css
@@ -0,0 +1,21 @@
+.polygon-area {
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+.polygons-header>>>.v-expansion-panel__header {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ padding-left: 5px;
+ min-height: 35px;
+ background: transparent;
+}
+
+.action-options-wrapper {
+ height: 45px;
+}
+
+.optimization-routes {
+ background: transparent;
+ border-bottom: 1px solid #cbced1;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-details/optimization-details.js b/src/fragments/forms/map-form/components/optimization/components/optimization-details/optimization-details.js
new file mode 100644
index 000000000..b0800b1d1
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-details/optimization-details.js
@@ -0,0 +1,105 @@
+import Download from '@/fragments/forms/map-form/components/download/Download'
+import Share from '@/fragments/forms/map-form/components/share/Share'
+import Print from '@/fragments/forms/map-form/components/print/Print'
+import MapViewData from '@/models/map-view-data'
+import OptimizationSteps from './components/optimization-steps/OptimizationSteps'
+import geoUtils from '@/support/geo-utils'
+import {getVehicleIconName, vehicleColors} from '@/support/optimization-utils'
+
+export default {
+ data: () => ({
+ localMapViewData: null,
+ panelExtended: [true, true, true],
+ }),
+ props: {
+ mapViewData: {
+ Type: MapViewData,
+ Required: true
+ }
+ },
+ components: {
+ Download,
+ Share,
+ Print,
+ OptimizationSteps,
+ },
+ computed: {
+ hasRoutes () {
+ return this.localMapViewData.isRouteData
+ },
+ shareUrl () {
+ return location.href
+ },
+ /**
+ * Builds and return the routes
+ * parsed, with translations and
+ * humanized content
+ * @returns {Array} of route objects
+ */
+ parsedRoutes () {
+ if (!this.hasRoutes) {
+ return []
+ }
+ const routes = []
+ for (const key in this.localMapViewData.routes) {
+ const route = {...this.localMapViewData.routes[key]}
+ if (!route.summary) {
+ route.summary = geoUtils.getHumanizedTimeAndDistance({distance: route.distance, duration:route.duration, unit: 'm'}, this.$t('global.units'))
+ this.parseSteps(route.steps)
+ route.distance = route.summary.distance
+ route.duration = route.summary.duration
+ }
+ routes.push(route)
+ }
+ return routes
+ },
+ unassignedJobsString () {
+ let ids = []
+ for (const job of this.mapViewData.rawData.unassigned) {
+ ids.push(job.id)
+ }
+ ids.sort((a,b) => a-b)
+ return 'Job ' + ids.join(', Job ')
+ }
+ },
+ created() {
+ this.localMapViewData = this.mapViewData.clone()
+ },
+ watch: {
+ /**
+ * Every time the response data changes
+ * the map builder is reset and the
+ * map data is reloaded
+ */
+ mapViewData: {
+ handler: function () {
+ this.localMapViewData = this.mapViewData.clone()
+ },
+ deep: true
+ },
+ },
+ methods: {
+ vehicleColors,
+ getVehicleIconName,
+ /** get the parsed segments by humanizing the duration and distances
+ * @param {*} steps
+ * @returns {Object} segments
+ */
+ parseSteps (steps) {
+ for (const step of steps) {
+ let {duration, distance} = geoUtils.getHumanizedTimeAndDistance({distance: step.distance, duration:step.duration, unit: 'm'}, this.$t('global.units'))
+ step.duration = duration
+ step.distance = distance
+ }
+ },
+ /**
+ * constructs the URL for forwarding to directions mode by route ID
+ * @param {number} routeId
+ */
+ generateRouteURL(routeId) {
+ const locationStrings = this.parsedRoutes[routeId].steps.map(e => [e.location[0].toFixed(7), e.location[1].toFixed(7)].toString())
+ const profile = this.localMapViewData.vehicles[routeId].profile
+ return `/#/directions/${locationStrings.join('/')}/data/{"coordinates":"${locationStrings.join(';')}","options":{"profile":"${profile}","preference":"recommended"}}`
+ }
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/OptimizationImport.vue b/src/fragments/forms/map-form/components/optimization/components/optimization-import/OptimizationImport.vue
new file mode 100644
index 000000000..bab5b9c91
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/OptimizationImport.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+ {{ content.header }}
+
+ save
+
+
+
+
+
+ {{$t('optimizationImport.acceptedImportTypes')}}: ors-json
+
+
+ {{$t('optimizationImport.acceptedImportTypes')}}: ors-json, GeoJSON, {{$t('global.and')}} csv
+
+
+
+
+
+
+
+
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.cs-cz.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.cs-cz.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.cs-cz.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.de-de.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.de-de.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.de-de.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.en-us.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.en-us.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.en-us.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.es-es.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.es-es.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.es-es.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.fr-fr.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.fr-fr.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.fr-fr.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.hu-hu.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.hu-hu.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.hu-hu.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.it-it.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.it-it.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.it-it.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.pt-br.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.pt-br.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.pt-br.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.ro-ro.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.ro-ro.js
new file mode 100644
index 000000000..40d8d573c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/i18n/optimization-import.i18n.ro-ro.js
@@ -0,0 +1,8 @@
+export default {
+ optimizationImport: {
+ acceptedImportTypes: 'Accepted import types',
+ notAJson: 'Input is not a valid JSON',
+ saveImport: 'Save new ',
+ notValid: 'File does not contain valid ',
+ }
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/optimization-import.css b/src/fragments/forms/map-form/components/optimization/components/optimization-import/optimization-import.css
new file mode 100644
index 000000000..ebea18155
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/optimization-import.css
@@ -0,0 +1,8 @@
+.edit-btn {
+ padding: 0 20px;
+ min-width: 0;
+ float: right;
+ margin: 0;
+ height: 24px;
+ background: white;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/components/optimization-import/optimization-import.js b/src/fragments/forms/map-form/components/optimization/components/optimization-import/optimization-import.js
new file mode 100644
index 000000000..fbd7bb21c
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/components/optimization-import/optimization-import.js
@@ -0,0 +1,239 @@
+import vueDropzone from 'vue2-dropzone'
+import constants from '@/resources/constants'
+import MapFormBtn from '@/fragments/forms/map-form-btn/MapFormBtn.vue'
+import 'vue2-dropzone/dist/vue2Dropzone.min.css'
+import Job from '@/models/job'
+import Vehicle from '@/models/vehicle'
+import Skill from '@/models/skill'
+
+export default {
+ data: () => ({
+ isImportOpen: true,
+ acceptedFiles: '.json,.csv,.geojson',
+ pastedData: [],
+ }),
+ props: {
+ expectedData: {
+ Type: String,
+ Required: true
+ },
+ },
+ components: {
+ vueDropzone,
+ MapFormBtn
+ },
+ computed: {
+ content () {
+ if (this.expectedData === 'jobs') {
+ return {
+ header: this.$t('optimization.import') + this.$t('optimization.jobs'),
+ saveImport: this.$t('optimizationImport.saveImport') + this.$t('optimization.jobs'),
+ jsonPlaceholder: '[{"id":1,"location":[8.68525,49.420822],"service":300,"delivery":[1],"skills":[1]}]',
+ }
+ } else if (this.expectedData === 'vehicles') {
+ return {
+ header: this.$t('optimization.import') + this.$t('optimization.vehicles'),
+ saveImport: this.$t('optimizationImport.saveImport') + this.$t('optimization.vehicles'),
+ jsonPlaceholder: '[{"id":1,"description":"","profile":"driving-car","start":[8.675863,49.418477],"end":[8.675863,49.418477],"capacity":[4],"skills":[1]}]',
+ }
+ } else if (this.expectedData === 'skills') {
+ return {
+ header: this.$t('optimization.import') + this.$t('optimization.skills'),
+ saveImport: this.$t('optimizationImport.saveSkillImport'),
+ jsonPlaceholder: '["{"name":"length over 1.5m","id":1}"]',
+ }
+ }
+ },
+ dropzoneOptions () {
+ return {
+ maxFilesize: 0.5,
+ url: 'https://not-used-url.tld', // declaration required by the component, but never used
+ uploadMultiple: false,
+ maxFiles: 1,
+ clickable: true,
+ acceptedFiles: this.acceptedFiles,
+ dictDefaultMessage: this.$t('routeImporter.dictDefaultMessage'),
+ dictFallbackMessage: this.$t('routeImporter.dictFallbackMessage'),
+ dictFileTooBig: this.$t('routeImporter.dictFileTooBig'),
+ dictInvalidFileType: this.$t('routeImporter.dictInvalidFileType'),
+ dictCancelUpload: this.$t('routeImporter.dictCancelUpload'),
+ dictUploadCanceled: this.$t('routeImporter.dictUploadCanceled'),
+ dictRemoveFile: this.$t('routeImporter.dictRemoveFile')
+ }
+ }
+ },
+ created() {
+ this.acceptedFiles = this.$root.appHooks.run('importerAcceptedFilesDefined', this.acceptedFiles)
+ },
+ methods: {
+ /**
+ * Handle the file added event
+ * @param {*} file
+ */
+ fileAdded (file) {
+ const context = this
+ const reader = new FileReader()
+ reader.addEventListener('loadend', function (event) {
+ const content = event.target.result
+ if (!content || content === 'null') {
+ context.showError(context.$t('routeImporter.failedToLoadFile'), {timeout: 0})
+ } else {
+ let parts = file.name.split('.')
+ let extension = parts.at(-1)
+ let type = file.type || extension
+ context.catchAndParseFile(content, type)
+ }
+ })
+ this.$refs.importRouteDropzone.removeAllFiles()
+ reader.readAsText(file)
+ },
+ /**
+ * Catch file contents and type, parse and call load
+ * @param {*} fileContent
+ * @param {*} type
+ */
+ catchAndParseFile (fileContent, type) {
+ let parsedInfos = null
+ let newSkills = []
+
+ if (type.indexOf('csv') > -1) {
+ parsedInfos = this.parseCsvFile(fileContent)
+ } else if (type.indexOf('json') > -1 || type.indexOf('geojson') > -1) {
+ const parsedJson = JSON.parse(fileContent)
+ if (parsedJson?.features) {
+ parsedInfos = this.parseGeojsonFile(parsedJson)
+ } else {
+ parsedInfos= this.parseJsonFile(parsedJson)
+ newSkills = parsedInfos.newSkills
+ }
+ }
+
+ if (parsedInfos) {
+ this.$emit('saveOptimizationImport', {jobs: parsedInfos.newJobs, vehicles: parsedInfos.newVehicles, skills: newSkills})
+ this.closeImporter()
+ } else {
+ this.showError(this.$t('routeImporter.failedToLoadFile'), {timeout: 0})
+ this.$emit('failedToImportFile')
+ }
+ },
+ parseCsvFile(fileContent) {
+ let newJobs = []
+ let newVehicles = []
+
+ if (this.expectedData === 'jobs') {
+ newJobs = Job.fromCsv(fileContent)
+ } else if (this.expectedData === 'vehicles') {
+ newVehicles = Vehicle.fromCsv(fileContent)
+ }
+ return {newJobs, newVehicles}
+ },
+ /**
+ * Parse geojson file
+ * @param parsedJson
+ * @returns {{newJobs: *[], newVehicles: *[]}}
+ */
+ parseGeojsonFile(parsedJson) {
+ let newJobs = []
+ let newVehicles = []
+
+ if (this.expectedData === 'jobs') {
+ for (const j of parsedJson.features) {
+ try {
+ newJobs.push(Job.fromGeoJsonObject(j))
+ } catch {
+ this.showError(this.$t('optimizationImport.notValid') + this.$t('optimization.jobs'),)
+ }
+ }
+ } else if (this.expectedData === 'vehicles') {
+ for (const v of parsedJson.features) {
+ try {
+ newVehicles.push(Vehicle.fromGeoJsonObject(v))
+ } catch {
+ this.showError(this.$t('optimizationImport.notValid') + this.$t('optimization.vehicles'),)
+ }
+ }
+ }
+ return {newJobs, newVehicles}
+ },
+ /**
+ * Parse json file
+ * @param parsedJson
+ * @returns {{newJobs: *[], newVehicles: *[], newSkills: *[]}}
+ */
+ parseJsonFile(parsedJson) {
+ let newJobs = []
+ let newVehicles = []
+ let newSkills = []
+
+ if (this.expectedData === 'jobs') {
+ newJobs = this.parseJsonObjects(parsedJson, newJobs, Job, 'jobs')
+ } else if (this.expectedData === 'vehicles') {
+ newVehicles = this.parseJsonObjects(parsedJson, newVehicles, Vehicle, 'vehicles')
+ } else if (this.expectedData === 'skills') {
+ for (const s of parsedJson) {
+ try {
+ newSkills.push(Skill.fromObject(s))
+ } catch {
+ this.showError(this.$t('optimizationImport.notValidSkill'))
+ }
+ }
+ }
+ return {newJobs, newVehicles, newSkills}
+ },
+ parseJsonObjects(parsedJson, newObjects, ObjectClass, item) {
+ for (const j of parsedJson) {
+ try {
+ newObjects.push(ObjectClass.fromObject(j))
+ } catch {
+ this.showError(this.$t('optimizationImport.notValid') + this.$t(`optimization.${item}`),)
+ }
+ }
+ return newObjects
+ },
+ // save jobs from pasted JSON and return error if not a valid JSON
+ savePastedJson() {
+ try {
+ const newJobs = []
+ const newVehicles = []
+ const newSkills = []
+ if (this.expectedData === 'jobs') {
+ for (const j of JSON.parse(this.pastedData)) {
+ newJobs.push(Job.fromObject(j))
+ }
+ } else if (this.expectedData === 'vehicles') {
+ for (const v of JSON.parse(this.pastedData)) {
+ newVehicles.push(Vehicle.fromObject(v))
+ }
+ } else if (this.expectedData === 'skills') {
+ for (const s of JSON.parse(this.pastedData)) {
+ newSkills.push(Skill.fromObject(s))
+ }
+ }
+ this.$emit('saveOptimizationImport', {jobs: newJobs, vehicles: newVehicles, skills: newSkills})
+ this.closeImporter()
+ } catch (err) {
+ this.showError(this.$t('optimizationImport.notAJson'), {timeout: 3000})
+ }
+ },
+
+ /**
+ * Send new map data via EventBus to ors-map
+ * @emits mapViewDataChanged via EventBus passing fileType and fileContent
+ * @param {*} fileType
+ * @param {*} fileContent
+ * @param {*} timestamp
+ */
+ sendDataToMap (fileType, fileContent, timestamp) {
+ const data = {
+ content: fileContent,
+ options: { origin: constants.dataOrigins.fileImporter, contentType: fileType, timestamp: timestamp }
+ }
+ this.$emit('contentUploaded', data)
+ },
+
+ // close the import dialog
+ closeImporter () {
+ this.$emit('close')
+ }
+ },
+}
diff --git a/src/fragments/forms/map-form/components/optimization/optimization.css b/src/fragments/forms/map-form/components/optimization/optimization.css
new file mode 100644
index 000000000..bf57a7ce3
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/optimization.css
@@ -0,0 +1,53 @@
+.optimization-heading {
+ font-size: medium;
+ padding: 10px 10px 0 10px;
+ font-weight: bold;
+ border-bottom: 1px solid #cbced1;
+}
+
+.hide-button {
+ padding: 0;
+ float: right;
+ margin-right: 20px
+}
+
+ul.job-inputs li {
+ list-style: none;
+}
+
+ul.job-inputs {
+ padding: 10px 0;
+}
+
+ul.vehicle-inputs li {
+ list-style: none;
+}
+
+ul.vehicle-inputs {
+ padding: 10px 0;
+}
+
+.route-btn {
+ float: right;
+ margin-right: 8px;
+ margin-top: 4px;
+}
+
+.skill-opt-btn {
+ float: left;
+ margin-left: 10px;
+ margin-right: 10px;
+ margin-top: 3px
+}
+.skill-btn-legend {
+ font-size: 9px;
+ text-align: center;
+ word-break: break-word;
+ max-width: 58px;
+ margin-left: 0;
+}
+
+.content-list {
+ box-shadow: none;
+ border-bottom: 2px solid #c62828;
+}
diff --git a/src/fragments/forms/map-form/components/optimization/optimization.js b/src/fragments/forms/map-form/components/optimization/optimization.js
new file mode 100644
index 000000000..b523bbef2
--- /dev/null
+++ b/src/fragments/forms/map-form/components/optimization/optimization.js
@@ -0,0 +1,603 @@
+import MapFormMixin from '../map-form-mixin'
+import MapViewDataBuilder from '@/support/map-data-services/map-view-data-builder'
+import FieldsContainer from '@/fragments/forms/fields-container/FieldsContainer'
+import FormActions from '@/fragments/forms/map-form/components/form-actions/FormActions'
+import {EventBus} from '@/common/event-bus'
+import {Optimization} from '@/support/ors-api-runner'
+import AppMode from '@/support/app-modes/app-mode'
+import MapViewData from '@/models/map-view-data'
+import constants from '@/resources/constants'
+import appConfig from '@/config/app-config'
+import Place from '@/models/place'
+import Job from '@/models/job'
+import Vehicle from '@/models/vehicle'
+import Skill from '@/models/skill'
+
+// Local components
+import OptimizationDetails from './components/optimization-details/OptimizationDetails'
+import OptimizationImport from './components/optimization-import/OptimizationImport.vue'
+import JobList from './components/item-list/JobList.vue'
+import VehicleList from './components/item-list/VehicleList.vue'
+import EditDialog from './components/edit-dialog/EditDialog.vue'
+import EditSkills from './components/edit-skills/EditSkills.vue'
+import theme from '@/config/theme'
+
+export default {
+ mixins: [MapFormMixin],
+ data: () => ({
+ active: true,
+ mode: constants.modes.optimization,
+ mapViewData: new MapViewData(),
+ skills: [],
+ jobs: [],
+ vehicles: [],
+ pickPlaceSupported: true,
+ showEditDialog: false,
+ editProp: '',
+ editId: 0,
+ editData: [],
+ isImportOpen: false,
+ expectedImport: '',
+ showSkillManagement: false,
+ jobsExpanded: true
+ }),
+ components: {
+ FieldsContainer,
+ FormActions,
+ OptimizationDetails,
+ OptimizationImport,
+ JobList,
+ VehicleList,
+ EditDialog,
+ EditSkills
+ },
+ computed: {
+ jobsJSON () {
+ const jsonJobs = []
+ for (const job of this.jobs) {
+ jsonJobs.push(job.toJSON())
+ }
+ return jsonJobs
+ },
+ vehiclesJSON () {
+ const jsonVehicles = []
+ for (const v of this.vehicles) {
+ jsonVehicles.push(v.toJSON())
+ }
+ return jsonVehicles
+ },
+ skillsJSON () {
+ const jsonSkills = []
+ for (const skill of this.skills) {
+ jsonSkills.push(skill.toJSON())
+ }
+ return jsonSkills
+ },
+ skillsInUse () {
+ let skills = []
+ for (const j of this.jobs) {
+ if (j.skills) {
+ skills.push(...j.skills)
+ }
+ }
+ for (const v of this.vehicles) {
+ if (v.skills) {
+ skills.push(...v.skills)
+ }
+ }
+ let skillIds = []
+ for (const s of skills) {
+ if (s && !skillIds.includes(s.id)) {
+ skillIds.push(s.id)
+ }
+ }
+ return skillIds
+ },
+ disabledActions () {
+ return appConfig.disabledActionsForOptimization
+ },
+ borderColor () {
+ return theme.primary || '#cbced1'
+ }
+ },
+ created () {
+ let storedSkills = localStorage.getItem('skills')
+ // load skills from local storage if there, otherwise set example skill
+ if (storedSkills) {
+ const skills = []
+ for (const s of JSON.parse(storedSkills)) {
+ skills.push(Skill.fromObject(s))
+ }
+ this.skills = skills
+ } else {
+ this.skills = [Skill.fromJSON('{"name":"cold chain", "id":1}')]
+ localStorage.setItem('skills', JSON.stringify(this.skillsJSON))
+ }
+ this.loadData()
+
+ const context = this
+ // When the filters object has changed externally, reprocess the app route
+ EventBus.$on('filtersChangedExternally', () => {
+ if (context.active) {
+ context.updateAppRoute()
+ }
+ })
+ // When the user click on a marker to remove it
+ EventBus.$on('removePlace', (data) => {
+ if (context.active) {
+ context.removePlace(data)
+ }
+ })
+
+ /**
+ * Update local object when a mapViewData is uploaded
+ */
+ EventBus.$on('mapViewDataUploaded', (mapViewData) => {
+ if (context.active) {
+ context.mapViewData = mapViewData
+ context.jobs = mapViewData.jobs
+ context.vehicles = mapViewData.vehicles
+ context.updateAppRoute()
+ }
+ })
+
+ /**
+ * If the map data view has changed and this component
+ * is not active, then reset its data to the initial state
+ */
+ EventBus.$on('mapViewDataChanged', () => {
+ if (!context.active) {
+ context.mapViewData = new MapViewData()
+ context.jobs = []
+ context.vehicles = []
+ }
+ })
+
+ // On map right click -> addJob
+ EventBus.$on('addJob', (data) => {
+ context.addJob(data)
+ })
+
+ // On popup edit click -> edit job
+ EventBus.$on('editJob', (index) => {
+ context.manageJobs(index)
+ })
+
+ // On map right click -> addVehicle
+ EventBus.$on('addVehicle', (data) => {
+ context.addVehicle(data)
+ })
+
+ // On popup edit click -> edit vehicle
+ EventBus.$on('editVehicle', (index) => {
+ context.manageVehicles(index)
+ })
+
+ // When a marker drag finishes, update
+ // the place coordinates and re-render the map
+ EventBus.$on('markerDragged', (marker) => {
+ if (context.active) {
+ if (marker.text.startsWith('V')) {
+ let vehicle = context.vehicles[parseInt(marker.text.slice(1))-1]
+ vehicle.setLngLat(marker.position.lng, marker.position.lat)
+ context.updateAppRoute()
+ } else {
+ let job = context.jobs[parseInt(marker.text)-1]
+ job.setLngLat(marker.position.lng, marker.position.lat)
+ context.updateAppRoute()
+ }
+ }
+ })
+
+ // place is picked from Map
+ EventBus.$on('setInputPlace', (data) => {
+ if (context.active) {
+ let id = data.placeInputId
+ // pickEditSource indicates which property the place should fill
+ if (data.pickEditSource === 'jobs') {
+ context.setJobLocation(id, data)
+ context.manageJobs(id)
+ } else if (data.pickEditSource === 'vehicleStart') {
+ context.setVehicleStartLocation(id, data)
+ context.manageVehicles(id)
+ } else if (data.pickEditSource === 'vehicleEnd') {
+ this.vehicles[data.pickPlaceIndex].end = data.place.coordinates
+ context.manageVehicles(id)
+ }
+ } else {
+ context.setSidebarIsOpen(true)
+ context.$forceUpdate()
+ }
+ })
+ },
+ watch: {
+ $route: function () {
+ if (this.$store.getters.mode === constants.modes.optimization) {
+ this.loadData()
+ } else {
+ this.skills = []
+ this.jobs = []
+ this.vehicles = []
+ }
+ },
+ '$store.getters.mode': function (activeMode) {
+ this.active = activeMode === this.mode
+ }
+ },
+ methods: {
+ /**
+ * When the user click on the map and select a point as the route start
+ * @param {*} data {latLng: ..., place:...}
+ */
+ addJob (data) {
+ const job = Job.fromPlace(data.place)
+ job.setId(this.jobs.length + 1)
+ const context = this
+ job.resolve().then(() => {
+ context.jobs.push(job)
+ context.manageJobs(job.id)
+ context.updateAppRoute()
+ }).catch((err) => {
+ console.log(err)
+ context.showError(this.$t('optimization.couldNotResolveTheLocation') + this.$t('optimization.jobs'), { timeout: 0 })
+ })
+ },
+ // open editJobs dialog
+ manageJobs(jobId) {
+ this.editProp = 'jobs'
+ this.editData = this.jobs
+ this.showEditDialog = true
+ this.editId = jobId
+ },
+ // when there are no jobs and button in sidebar is clicked
+ addJobFromMap() {
+ this.showInfo(this.$t('placeInput.clickOnTheMapToSelectAPlace'))
+ this.setPickPlaceSource(this.jobs)
+ },
+
+ // when the user clicks on the map and selects a point as the route start
+ addVehicle (data) {
+ const vehicle = Vehicle.fromPlace(data.place)
+ vehicle.setId(this.vehicles.length + 1)
+ const context = this
+ vehicle.resolve().then(() => {
+ if (this.vehicles.length > 3) {
+ this.showError(this.$t('optimization.maxWarning') + '3' + this.$t('optimization.vehicles'), {timeout: 3000})
+ }
+ context.vehicles.push(vehicle)
+ context.manageVehicles(vehicle.id)
+ context.updateAppRoute()
+ }).catch((err) => {
+ console.log(err)
+ context.showError(this.$t('optimization.couldNotResolveTheLocation') + this.$t('optimization.vehicles'), { timeout: 0 })
+ })
+ },
+ // open editVehicles dialog
+ manageVehicles(vehicleId) {
+ this.editProp = 'vehicles'
+ this.editData = this.vehicles
+ this.showEditDialog = true
+ this.editId = vehicleId
+ },
+ // when there are no vehicles and button in sidebar is clicked
+ addVehicleFromMap() {
+ this.showInfo(this.$t('placeInput.clickOnTheMapToSelectAPlace'))
+ this.setPickPlaceSource(this.vehicles)
+ },
+
+ // save vehicles from pasted JSON and return error if not a valid JSON
+ saveImport(data) {
+ if (data.jobs.length) {
+ this.jobsChanged(data.jobs)
+ } else if (data.vehicles.length){
+ this.vehiclesChanged(data.vehicles)
+ }
+ this.isImportOpen = false
+ this.expectedImport = ''
+ },
+
+ // open editSkills dialog
+ manageSkills(skillId) {
+ this.showSkillManagement = true
+ EventBus.$emit('showSkillsModal', skillId)
+ },
+
+ // Set the pick place input source
+ setPickPlaceSource (source) {
+ if (this.pickPlaceSupported) {
+ this.$store.commit('pickPlaceIndex', source.length)
+ this.$store.commit('pickPlaceId', source.length + 1)
+ if (source === this.jobs) {
+ this.$store.commit('pickEditSource', 'jobs')
+ } else if (source === this.vehicles) {
+ this.$store.commit('pickEditSource', 'vehicleStart')
+ }
+ }
+ },
+ // remove job or vehicle when marker is deleted from map view
+ removePlace (data) {
+ if (data.job) {
+ let index = data.job.id - 1
+ this.jobs.splice(index,1)
+ for (const i in this.jobs) {
+ this.jobs[i].setId(parseInt(i)+1)
+ }
+ } else if (data.vehicle) {
+ let index = data.vehicle.id - 1
+ this.vehicles.splice(index,1)
+ for (const i in this.vehicles) {
+ this.vehicles[i].setId(parseInt(i)+1)
+ }
+ }
+ this.updateAppRoute()
+ },
+ /**
+ * Set location of job with id from given data or create new Job
+ * @param id
+ * @param data
+ */
+ setJobLocation(id, data){
+ if (id > this.jobs.length) {
+ let job = Job.fromPlace(data.place)
+ job.setId(id)
+ this.jobs.push(job)
+ } else {
+ this.jobs[data.pickPlaceIndex].location = data.place.coordinates
+ }
+ },
+ /**
+ * Set start location of vehicle with id from given data or create new Job
+ * @param id
+ * @param data
+ */
+ setVehicleStartLocation(id, data){
+ if (id > this.vehicles.length) {
+ let v = Vehicle.fromPlace(data.place)
+ v.setId(id)
+ this.vehicles.push(v)
+ } else {
+ const v = this.vehicles[data.pickPlaceIndex]
+ if (v.end[0] === v.start[0] && v.end[1] === v.start[1]) {
+ this.vehicles[data.pickPlaceIndex].end = data.place.coordinates
+ }
+ this.vehicles[data.pickPlaceIndex].start = data.place.coordinates
+ }
+ },
+ /**
+ * After each change on the map search we redirect the user to the built target app route
+ * The data will be loaded from the path and the map will be updated, keeping the
+ * url synchronized with the current map status
+ */
+ updateAppRoute () {
+ this.$store.commit('mode', constants.modes.optimization)
+ const appMode = new AppMode(this.$store.getters.mode)
+ let jobLocations = []
+ let jobProps = []
+ for (const job of this.jobs) {
+ jobLocations.push(Place.fromJob(job))
+ jobProps.push(this.getPropsFromJob(job))
+ }
+ const route = appMode.getRoute(jobLocations, {vehicles: this.vehiclesJSON, jobProps: jobProps})
+ if (Object.keys(route.params).length > 1) {// params contains data and placeName? props
+ this.$router.push(route)
+ } else {
+ this.optimizeJobs()
+ }
+ },
+ getPropsFromJob (job) {
+ let jobProps = {id: job.id}
+ if (job.skills.length) {
+ let skillIds = []
+ for (const skill of job.skills) {
+ skillIds.push(skill.id)
+ }
+ skillIds.sort((a,b) => a-b)
+ jobProps.skills = skillIds
+ }
+ for (const prop of ['service', 'priority', 'delivery', 'pickup']) {
+ if (job[prop] && job[prop] !== 0 && job[prop][0] !== 0) {
+ jobProps[prop] = job[prop]
+ }
+ }
+ return jobProps
+ },
+ parsePropSkills(propsOfJob) {
+ let propSkills = []
+ for (const s of propsOfJob.skills) {
+ let skillIds = []
+ for (const skill of this.skills) {
+ skillIds.push(skill.id)
+ }
+ if (skillIds.includes(s)) {
+ propSkills.push(this.skills[s - 1])
+ } else {
+ propSkills.push(new Skill('Skill from added ' + this.$t('optimization.job') + ' ' + propsOfJob.id, s))
+ }
+ }
+ return propSkills
+ },
+ parseProps(jobProps) {
+ let parsedProps = []
+ for (const j of jobProps) {
+ let parsedJobProps = {id: j.id}
+ for (const prop of ['service', 'priority', 'delivery', 'pickup', 'time_windows']) {
+ if (j[prop]) {
+ parsedJobProps[prop] = j[prop]
+ }
+ }
+ if (j.skills) {
+ parsedJobProps.skills = this.parsePropSkills(j)
+ }
+
+ parsedProps.push(parsedJobProps)
+ }
+ return parsedProps
+ },
+ /**
+ * Request and draw a route based on the value of multiples places input
+ * @returns {Promise}
+ */
+ optimizeJobs () {
+ localStorage.setItem('jobs', JSON.stringify(this.jobsJSON))
+ localStorage.setItem('vehicles', JSON.stringify(this.vehiclesJSON))
+ const context = this
+ return new Promise((resolve) => {
+ if (context.jobs.length) {
+ if (context.vehicles.length) {
+ context.showInfo(context.$t('optimization.optimize') + this.$t('optimization.jobs'), { timeout: 0 })
+ EventBus.$emit('showLoading', true)
+
+ // Calculate the optimized routes
+ Optimization(context.jobs, context.vehicles).then(data => {
+ data.options.translations = context.$t('global.units')
+
+ data = context.$root.appHooks.run('beforeBuildOptimizationMapViewData', data)
+ if (data) {
+ MapViewDataBuilder.buildMapData(data, context.$store.getters.appRouteData).then((mapViewData) => {
+ context.mapViewData = mapViewData
+ context.mapViewData.mode = constants.modes.optimization
+ context.mapViewData.jobs = context.jobs
+ context.mapViewData.vehicles = context.vehicles
+ EventBus.$emit('mapViewDataChanged', mapViewData)
+ EventBus.$emit('newInfoAvailable')
+ context.showSuccess(context.$t('optimization.optimizationResultReady'), { timeout: 2000 })
+ context.setSidebarIsOpen()
+ resolve(mapViewData)
+ })
+ }
+ }).catch(result => {
+ context.handleOptimizeJobsError(result)
+ }).finally(() => {
+ EventBus.$emit('showLoading', false)
+ })
+ } else {
+ context.showError(context.$t('optimization.vehicles') + context.$t('optimization.nothingToManage'))
+ EventBus.$emit('updateOnlyMarkers', {jobs: context.jobs, vehicles: context.vehicles})
+ }
+ } else {
+ if (context.vehicles.length) {
+ context.showError(context.$t('optimization.jobs') + context.$t('optimization.nothingToManage'))
+ }
+ EventBus.$emit('updateOnlyMarkers', {jobs: context.jobs, vehicles: context.vehicles})
+ // There are no enough places or round trip to be routed
+ resolve({})
+ }
+ })
+ },
+ /**
+ * Handle the route places error response displaying the correct message
+ * @param {*} result
+ */
+ handleOptimizeJobsError (result) {
+ this.$root.appHooks.run('beforeHandleOptimizationError', result)
+
+ const errorCode = this.lodash.get(result, constants.responseErrorCodePath)
+ if (errorCode) {
+ const errorKey = `optimization.apiError.${errorCode}`
+ let errorMsg = this.$t(errorKey)
+ if (errorMsg === errorKey) {
+ errorMsg = this.$t('optimization.genericErrorMsg')
+ }
+ this.showError(errorMsg, { timeout: 0, mode: 'multi-line' })
+ console.error(result.response.error)
+ } else {
+ this.showError(this.$t('optimization.notPossible'), { timeout: 0 })
+ console.error(result)
+ }
+ },
+
+ /**
+ * Load the map data from the url
+ * rebuilding the place inputs, and its values
+ * and render the map with this data (place or route)
+ */
+ loadData () {
+ if (this.$store.getters.mode === constants.modes.optimization) {
+ this.loadVehicles()
+ this.loadJobs()
+ this.optimizeJobs()
+ }
+ },
+
+ /**
+ * Load data of vehicles
+ * prioritizing url data over storage data
+ */
+ loadVehicles() {
+ const defaultVehicles = this.vehicles
+ const urlVehicles = this.$store.getters.appRouteData.options.vehicles
+ let storedVehicles = localStorage.getItem('vehicles')
+ // prioritise data from url, then data from local storage
+ if (urlVehicles) {
+ const vehicles = []
+ for (let v of urlVehicles) {
+ vehicles.push(Vehicle.fromObject(v))
+ }
+ this.vehicles = vehicles
+ } else if (this.vehicles === undefined && storedVehicles) {
+ const vehicles = []
+ for (const v of JSON.parse(storedVehicles)) {
+ vehicles.push(Vehicle.fromObject(v))
+ }
+ this.vehicles = vehicles
+ } else if (this.vehicles === undefined || !this.vehicles.length) {
+ this.vehicles = defaultVehicles
+ }
+ },
+
+ /**
+ * Load data of jobs
+ * prioritizing url data over storage data
+ */
+ loadJobs() {
+ // Empty the array and populate it with the
+ // places from the appRoute without changing the
+ // object reference because it is a prop
+ const defaultJobs = this.jobs
+ const places = this.$store.getters.appRouteData.places
+ const propData = this.$store.getters.appRouteData.options.jobProps
+ let storedJobs = localStorage.getItem('jobs')
+ const jobs = []
+ if (propData && places.length === propData.length) {
+ const jobProps = this.parseProps(propData)
+ for (const [i, place] of places.entries()) {
+ jobs.push(new Job(place.lng, place.lat, place.placeName, jobProps[i]))
+ }
+ } else if (this.jobs === undefined && storedJobs) {
+ for (const job of JSON.parse(storedJobs)) {
+ jobs.push(Job.fromObject(job))
+ }
+ }
+ this.jobs = jobs
+ if (!this.jobs.length) {
+ this.jobs = defaultJobs
+ }
+ },
+ // when jobs are changed update jobs and generate new route
+ jobsChanged(editedJobs) {
+ let newJobs = []
+ for (const job of editedJobs) {
+ newJobs.push(job.clone())
+ }
+ this.jobs = newJobs
+ this.updateAppRoute()
+ },
+ // when vehicles are changed update vehicles and generate new route
+ vehiclesChanged(editedVehicles) {
+ let newVehicles = []
+ for (const vehicle of editedVehicles) {
+ newVehicles.push(vehicle.clone())
+ }
+ this.vehicles = newVehicles
+ this.updateAppRoute()
+ },
+ // when skills are changed update skills
+ skillsChanged(editedSkills) {
+ let newSkills = []
+ for (const skill of editedSkills) {
+ newSkills.push(skill.clone())
+ }
+ this.skills = newSkills
+ },
+ }
+}
diff --git a/src/fragments/forms/map-form/components/place-and-directions/components/route-details/RouteDetails.vue b/src/fragments/forms/map-form/components/place-and-directions/components/route-details/RouteDetails.vue
index 5233dad85..f102ffc41 100644
--- a/src/fragments/forms/map-form/components/place-and-directions/components/route-details/RouteDetails.vue
+++ b/src/fragments/forms/map-form/components/place-and-directions/components/route-details/RouteDetails.vue
@@ -2,7 +2,7 @@
-
+
{{$t('routeDetails.routeDetails')}}
diff --git a/src/fragments/forms/map-form/components/place-and-directions/places-and-directions.js b/src/fragments/forms/map-form/components/place-and-directions/places-and-directions.js
index ce3b00a35..39d266abf 100644
--- a/src/fragments/forms/map-form/components/place-and-directions/places-and-directions.js
+++ b/src/fragments/forms/map-form/components/place-and-directions/places-and-directions.js
@@ -1,9 +1,12 @@
+import MapFormMixin from '../map-form-mixin'
import MapViewDataBuilder from '@/support/map-data-services/map-view-data-builder'
import FieldsContainer from '@/fragments/forms/fields-container/FieldsContainer'
import OrsParamsParser from '@/support/map-data-services/ors-params-parser'
import OrsFilterUtil from '@/support/map-data-services/ors-filter-util'
-import PlaceInput from '@/fragments/forms/place-input/PlaceInput.vue'
+import FormActions from '../form-actions/FormActions'
import MapFormBtn from '@/fragments/forms/map-form-btn/MapFormBtn'
+import PlaceInput from '@/fragments/forms/place-input/PlaceInput.vue'
+import {EventBus} from '@/common/event-bus'
import {Directions} from '@/support/ors-api-runner'
import AppMode from '@/support/app-modes/app-mode'
import MapViewData from '@/models/map-view-data'
@@ -19,28 +22,26 @@ import AltitudePreview from './components/altitude-preview/AltitudePreview'
import RouteDetails from './components/route-details/RouteDetails.vue'
import PlaceDetails from './components/place-details/PlaceDetails.vue'
import RoundTrip from './components/round-trip/RoundTrip.vue'
-import FormActions from '../form-actions/FormActions'
-import MapFormMixin from '../map-form-mixin'
-import {EventBus} from '@/common/event-bus'
export default {
mixins: [MapFormMixin],
data: () => ({
+ active: true,
mapViewData: new MapViewData(),
places: [new Place()],
roundTripActive: false,
placeFocusIndex: null
}),
components: {
+ FieldsContainer,
+ FormActions,
+ MapFormBtn,
PlaceInput,
- PlaceDetails,
- RouteDetails,
Draggable,
- FieldsContainer,
AltitudePreview,
- RoundTrip,
- FormActions,
- MapFormBtn
+ RouteDetails,
+ PlaceDetails,
+ RoundTrip
},
created () {
this.setListeners()
@@ -51,6 +52,9 @@ export default {
if (newVal === true && this.places.length === 1) {
this.setFocusedPlaceInput(0)
}
+ },
+ '$store.getters.mode': function (activeMode) {
+ this.active = activeMode === constants.modes.place || activeMode === constants.modes.directions
}
},
computed: {
diff --git a/src/fragments/forms/map-form/i18n/map-form.i18n.de-de.js b/src/fragments/forms/map-form/i18n/map-form.i18n.de-de.js
index aa3bb5a94..06499c058 100755
--- a/src/fragments/forms/map-form/i18n/map-form.i18n.de-de.js
+++ b/src/fragments/forms/map-form/i18n/map-form.i18n.de-de.js
@@ -3,6 +3,7 @@ export default {
mapForm: {
placesAndDirections: 'Suche & Los',
isochrones: 'Erreichbarkeit',
+ optimization: 'Optimize (Preview)',
uploadedContentRendered: 'Hochgeladene Inhalte gerendert',
errorRenderingContentUploaded: 'Fehler beim Hochladen der Inhalte. Bitte überprüfen Sie das Dateiformat und den Inhalt.'
}
diff --git a/src/fragments/forms/map-form/i18n/map-form.i18n.en-us.js b/src/fragments/forms/map-form/i18n/map-form.i18n.en-us.js
index 9c29761e6..b2154af93 100755
--- a/src/fragments/forms/map-form/i18n/map-form.i18n.en-us.js
+++ b/src/fragments/forms/map-form/i18n/map-form.i18n.en-us.js
@@ -3,6 +3,7 @@ export default {
mapForm: {
placesAndDirections: 'Find & go',
isochrones: 'Reach',
+ optimization: 'Optimize (Preview)',
uploadedContentRendered: 'Uploaded content rendered',
errorRenderingContentUploaded: 'Error while rendering the content uploaded. Check the file format and content'
}
diff --git a/src/fragments/forms/map-form/map-form.js b/src/fragments/forms/map-form/map-form.js
index 2885cf5e4..cb2c9a6f7 100644
--- a/src/fragments/forms/map-form/map-form.js
+++ b/src/fragments/forms/map-form/map-form.js
@@ -1,5 +1,6 @@
import PlacesAndDirections from './components/place-and-directions/PlacesAndDirections'
import Isochrones from './components/isochrones/Isochrones'
+import Optimization from './components/optimization/Optimization'
import resolver from '@/support/routes-resolver'
import constants from '@/resources/constants'
import appConfig from '@/config/app-config'
@@ -10,7 +11,8 @@ export default {
}),
components: {
PlacesAndDirections,
- Isochrones
+ Isochrones,
+ Optimization
},
watch: {
activeTab: function () {
@@ -32,6 +34,9 @@ export default {
},
hasIsochronesTab () {
return appConfig.supportsIsochrones
+ },
+ hasOptimizationTab () {
+ return appConfig.supportsOptimization
}
},
methods: {
@@ -40,7 +45,7 @@ export default {
*/
tabChanged () {
if (this.activeTab === 0) {
- if (this.$store.getters.mode === constants.modes.isochrones) {
+ if ([constants.modes.isochrones, constants.modes.optimization].includes(this.$store.getters.mode)) {
if (this.$route.fullPath.includes(resolver.directions())) {
this.$store.commit('mode', constants.modes.directions)
} else {
@@ -49,6 +54,8 @@ export default {
}
} else if (this.activeTab === 1) {
this.$store.commit('mode', constants.modes.isochrones)
+ } else if (this.activeTab === 2) {
+ this.$store.commit('mode', constants.modes.optimization)
}
},
/**
@@ -57,14 +64,23 @@ export default {
* and render the map with this data (place or route)
*/
setTab () {
- if (!this.hasPlacesAndDirectionsTab) (
- this.$store.commit('mode', constants.modes.isochrones)
- )
+ if (!this.hasPlacesAndDirectionsTab) {
+ if (!this.hasIsochronesTab) {
+ this.$store.commit('mode', constants.modes.optimization)
+ } else {
+ this.$store.commit('mode', constants.modes.isochrones)
+ }
+ }
if (this.hasIsochronesTab && this.$store.getters.mode === constants.modes.isochrones) {
this.activeTab = 1
if (this.$mdAndUpResolution && !this.$store.getters.embed) {
this.$store.commit('setLeftSideBarIsOpen', true)
}
+ } else if (this.hasOptimizationTab && this.$store.getters.mode === constants.modes.optimization) {
+ this.activeTab = 2
+ if (this.$mdAndUpResolution && !this.$store.getters.embed) {
+ this.$store.commit('setLeftSideBarIsOpen', true)
+ }
} else {
this.activeTab = 0
diff --git a/src/fragments/forms/place-input/PlaceAutocomplete.vue b/src/fragments/forms/place-input/PlaceAutocomplete.vue
new file mode 100644
index 000000000..d7af4703e
--- /dev/null
+++ b/src/fragments/forms/place-input/PlaceAutocomplete.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+ map
+
+
+
+
+
+
+ location_city
+
+ place
+
+
+
+
+ {{highlightedName(placeSuggested.placeName)}}
+
+
+
+ {{ getLayerTranslation(placeSuggested.properties.layer) }}
+ - {{ placeSuggested.properties.locality }}
+ - {{ placeSuggested.properties.country }}
+
+ ~{{distance(placeSuggested)}}
+ {{$t('global.units.' + $store.getters.mapSettings.unit)}}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.cs-cz.js b/src/fragments/forms/place-input/i18n/place-input.i18n.cs-cz.js
index 44a37b73b..b3613520f 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.cs-cz.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.cs-cz.js
@@ -2,6 +2,7 @@ export default {
placeInput: {
findAPlace: 'NajÃt mÃsto',
place: 'MÃsto',
+ location: 'Location',
addRouteStop: 'Přidat zastávku na trase',
routePlace: 'Zastávka na trase',
routeDestination: 'CÃl',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.de-de.js b/src/fragments/forms/place-input/i18n/place-input.i18n.de-de.js
index a0088a1a9..eb0cdf024 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.de-de.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.de-de.js
@@ -3,6 +3,7 @@ export default {
placeInput: {
findAPlace: 'Ort finden',
place: 'Ort',
+ location: 'Standort',
addRouteStop: 'Wegpunkt hinzufügen',
routePlace: 'Wegpunkt',
routeDestination: 'Ziel',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.en-us.js b/src/fragments/forms/place-input/i18n/place-input.i18n.en-us.js
index fb4836415..6fee10060 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.en-us.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.en-us.js
@@ -3,6 +3,7 @@ export default {
placeInput: {
findAPlace: 'Find a place',
place: 'Place',
+ location: 'Location',
addRouteStop: 'Add a route stop',
routePlace: 'Route stop',
routeDestination: 'Destination',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.es-es.js b/src/fragments/forms/place-input/i18n/place-input.i18n.es-es.js
index f7445618f..b18db93d6 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.es-es.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.es-es.js
@@ -2,6 +2,7 @@ export default {
placeInput: {
'findAPlace': 'Buscar por lugares',
'place': 'Local',
+ 'location': 'Location',
'addRouteStop': 'Añadir una parada',
'routePlace': 'Parada',
'routeDestination': 'Destino',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.fr-fr.js b/src/fragments/forms/place-input/i18n/place-input.i18n.fr-fr.js
index c9b3242de..8e05eb0e4 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.fr-fr.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.fr-fr.js
@@ -3,6 +3,7 @@ export default {
'placeInput': {
'findAPlace': 'Trouver un lieu',
'place': 'Lieu',
+ 'location': 'Location',
'addRouteStop': 'Ajouter un arrêt',
'routePlace': 'Arrêt de l\'itinéraire',
'routeDestination': 'Destination',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.hu-hu.js b/src/fragments/forms/place-input/i18n/place-input.i18n.hu-hu.js
index 4776aa0eb..b5eaca0e2 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.hu-hu.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.hu-hu.js
@@ -3,6 +3,7 @@ export default {
placeInput: {
'findAPlace': 'Hely keresése',
'place': 'Hely',
+ 'location': 'Location',
'addRouteStop': 'Köztes úticél hozzáadása',
'routePlace': 'Köztes úticél',
'routeDestination': 'Úticél',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.it-it.js b/src/fragments/forms/place-input/i18n/place-input.i18n.it-it.js
index e70915200..83a00b584 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.it-it.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.it-it.js
@@ -2,6 +2,7 @@ export default {
placeInput: {
'findAPlace': 'Trova un luogo',
'place': 'Luogo',
+ 'location': 'Location',
'addRouteStop': 'Aggiungi un punto',
'routePlace': 'Punto',
'routeDestination': 'Destinazione',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.pt-br.js b/src/fragments/forms/place-input/i18n/place-input.i18n.pt-br.js
index 8987f837d..154917805 100755
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.pt-br.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.pt-br.js
@@ -3,6 +3,7 @@ export default {
placeInput: {
findAPlace: 'Buscar por local',
place: 'Local',
+ location: 'Location',
addRouteStop: 'Adicionar uma parada',
routePlace: 'Parada',
routeDestination: 'Destino',
diff --git a/src/fragments/forms/place-input/i18n/place-input.i18n.ro-ro.js b/src/fragments/forms/place-input/i18n/place-input.i18n.ro-ro.js
index d9887762d..ffbee41cc 100644
--- a/src/fragments/forms/place-input/i18n/place-input.i18n.ro-ro.js
+++ b/src/fragments/forms/place-input/i18n/place-input.i18n.ro-ro.js
@@ -2,6 +2,7 @@ export default {
placeInput: {
findAPlace: 'Găsiți un loc',
place: 'Locul',
+ location: 'Location',
addRouteStop: 'Adăugați o oprire pe traseu',
routePlace: 'Oprire traseu',
routeDestination: 'Destinație',
diff --git a/src/fragments/forms/place-input/place-autocomplete.css b/src/fragments/forms/place-input/place-autocomplete.css
new file mode 100644
index 000000000..f9d271dc1
--- /dev/null
+++ b/src/fragments/forms/place-input/place-autocomplete.css
@@ -0,0 +1,4 @@
+.append-input-btn {
+ margin-top: -7px;
+ padding-left: 15px;
+}
diff --git a/src/fragments/forms/place-input/place-autocomplete.js b/src/fragments/forms/place-input/place-autocomplete.js
new file mode 100644
index 000000000..0b77bba65
--- /dev/null
+++ b/src/fragments/forms/place-input/place-autocomplete.js
@@ -0,0 +1,336 @@
+import {PlacesSearch, ReverseGeocode} from '@/support/ors-api-runner'
+import Utils from '@/support/utils'
+import GeoUtils from '@/support/geo-utils'
+import appConfig from '@/config/app-config'
+import {EventBus} from '@/common/event-bus'
+import Job from '@/models/job'
+import Vehicle from '@/models/vehicle'
+import Place from '@/models/place'
+
+export default {
+ data: () => ({
+ model: new Place(),
+ localModel: null,
+ editSource: null,
+ focused: false,
+ searching: false,
+ debounceTimeoutId: null,
+ pickPlaceSupported: true,
+ showEditBox: true
+ }),
+ props: {
+ editId: {
+ Type: Array,
+ Required: true
+ },
+ jobs: {
+ Type: Array[Job],
+ Required: false
+ },
+ vehicles: {
+ Type: Array[Vehicle],
+ Required: false
+ },
+ newEndPoint: {
+ Type: Boolean,
+ Required: false
+ },
+ onlyStartPoint: {
+ Type: Boolean,
+ Required: false
+ },
+ },
+ components: {
+ EventBus
+ },
+ computed: {
+ // Return an array with the place's suggestion based on the model suggestion data
+ placeSuggestions () {
+ if (!this.focused) {
+ return []
+ }
+ let suggestions = []
+ if (this.localModel.nameIsNumeric()) {
+ const latLng = this.model.getLatLng()
+ const rawCoordinatesPlace = new Place(latLng.lng, latLng.lat, `${latLng.lng},${latLng.lat}`, { properties: { layer: 'rawCoordinate' } })
+ rawCoordinatesPlace.rawCoordinate = true
+ suggestions.push(rawCoordinatesPlace)
+ }
+ suggestions = suggestions.concat(this.localModel.suggestions)
+ return suggestions
+ },
+ appendBtn () {
+ if (this.$lowResolution || this.localModel.isEmpty()) {
+ return 'map'
+ }
+ }
+ },
+ created() {
+ this.localModel = this.model.clone()
+ this.getImgSrc = Utils.getImgSrc
+
+ this.setSource()
+ },
+ methods: {
+ setFocus (data) {
+ // When the user clicks outside an input this method is called and is intended to
+ // set the focus as false in this case. To do so, we check if the was previously focused
+ // The parameters passed (automatically) by the click-outside is expected to be MouseEvent object and not a boolean.
+ if (typeof data === 'object' && data.clickedOutside) {
+ if (this.inputWasActiveAndLostFocus(data)) {
+ this.emptyPickPlaceSource()
+ this.focused = false
+ }
+ } else {
+ this.focused = data // data is boolean in this case
+ }
+ // If the job location is in search mode, then run the autocompleteSearch that will show the suggestions
+ if (this.focused) {
+ this.autocompleteSearch()
+ }
+ },
+ // set the parent source by checking which property was handed into this component
+ setSource () {
+ if (this.jobs) {
+ this.editSource = 'jobs'
+ } else if (this.newEndPoint) {
+ this.editSource = 'vehicleEnd'
+ } else if (this.vehicles) {
+ this.editSource = 'vehicleStart'
+ }
+ },
+
+ // Handle the click on the pick a place from the map btn
+ pickPlaceMapClick (event) {
+ this.showInfo(this.$t('placeInput.clickOnTheMapToSelectAPlace'))
+ this.localModel = new Place()
+
+ this.showEditBox = false
+ EventBus.$emit('pickAPlace')
+
+ this.setPickPlaceSource()
+ event.stopPropagation()
+ event.preventDefault()
+ },
+ // Set the pick place input source
+ setPickPlaceSource () {
+ if (this.pickPlaceSupported) {
+ this.$store.commit('pickPlaceIndex', this.editId - 1)
+ this.$store.commit('pickPlaceId', this.editId)
+ this.$store.commit('pickEditSource', this.editSource)
+ }
+ },
+ // Empty the pick place source
+ emptyPickPlaceSource() {
+ this.$store.commit('pickPlaceIndex', null)
+ this.$store.commit('pickPlaceId', null)
+ this.$store.commit('pickEditSource', null)
+ },
+ // highlight typed place name
+ highlightedName (placeName) {
+ let searchMask = this.localModel.placeName
+ const regEx = new RegExp(searchMask, 'ig')
+ let localPlaceName = this.localModel.placeName
+ let replaceMask
+ if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase() + ' ') === 0) {
+ localPlaceName = localPlaceName[0].toUpperCase() + localPlaceName.substring(1) + ' '
+ } else if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase()) === 0 ) {
+ localPlaceName = localPlaceName[0].toUpperCase() + localPlaceName.substring(1)
+ } else if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase()) > 0 ) {
+ localPlaceName = ' ' + localPlaceName[0].toUpperCase() + localPlaceName.substring(1)
+ }
+ replaceMask = `
${localPlaceName}`
+
+ placeName = placeName.replace(regEx, replaceMask)
+ return placeName.trim()
+ },
+ showAreaIcon (place) {
+ return place.properties.layer === 'country' || place.properties.layer === 'region'
+ },
+ // Get layer translation based on the layer name or fall back to a default one if not available
+ getLayerTranslation (layer) {
+ let transKey = 'global.layers.'+ layer
+ let translation = this.$t(transKey)
+ if (translation !== transKey) {
+ return translation
+ } else {
+ return this.$t('global.layers.notAvailable')
+ }
+ },
+ // Get the distance between the current map center and a suggestion location
+ distance (suggestedPlace) {
+ // Set origin and destination
+ const fromLatLng = { lat: this.$store.getters.mapCenter.lat, lng: this.$store.getters.mapCenter.lng }
+ const toLatLng = { lat: suggestedPlace.lat, lng: suggestedPlace.lng }
+
+ // calculate the distance between the two points
+ let distance = GeoUtils.calculateDistanceBetweenLocations(fromLatLng, toLatLng, this.$store.getters.mapSettings.unit)
+
+ if (distance > 0) {
+ distance = distance.toFixed(1)
+ } else {
+ distance = 0
+ }
+ return distance
+ },
+ // Set a suggestion item clicked as selected and emit the selected event
+ selectSuggestion (suggestedPlace) {
+ // Only proceed if it is being selected
+ // a place different from the current one
+ if (!suggestedPlace.equals(this.model)) {
+ // If the suggested place is a ra coordinate, remove the layer attribute
+ // because it is a placeholder, not a valid layer
+ if (suggestedPlace.rawCoordinate) {
+ delete suggestedPlace.properties.layer
+ }
+ this.selectPlace(suggestedPlace)
+ this.$forceUpdate()
+ this.selected()
+ }
+ },
+ // Set a suggested place as the selected one for a given place input
+ selectPlace (place) {
+ // We shall not reassign an external object, so we update each property
+ this.model.placeName = place.properties.label || place.placeName
+ this.model.placeId = place.properties.id
+ this.model.setLngLat(place.lng, place.lat)
+ this.model.properties = place.properties
+ this.model.suggestions = []
+ this.searching = false
+ // If a place is selected from a suggestion then no current location must be active.
+ this.$store.commit('currentLocation', null)
+ },
+ // Run the place selection hook and emit the selected event
+ selected () {
+ this.focused = false
+ if (this.editSource === 'jobs') {
+ this.jobs[this.editId-1].location = this.model.coordinates
+ } else if (['vehicleStart', 'vehicleEnd'].includes(this.editSource)) {
+ if (!this.newEndPoint) {
+ if (this.onlyStartPoint) {
+ this.vehicles[this.editId-1].start = this.model.coordinates
+ this.onlyStartPoint = false
+ } else {
+ this.vehicles[this.editId-1].start = this.model.coordinates
+ this.vehicles[this.editId-1].end = this.model.coordinates
+ }
+ } else {
+ this.vehicles[this.editId-1].end = this.model.coordinates
+ this.newEndPoint = false
+ }
+ }
+ },
+ // Handles the input change with a debounce-timeout
+ locationInputChanged (event = null) {
+ this.localModel = this.model.clone()
+ if (event) {
+ const isPasteEvent = event instanceof ClipboardEvent
+ // In case of a ClipboardEvent (ctr + v) we must just ignore, since the input
+ // model has not changed yet, and it will trigger another change event when it changes
+ if (!isPasteEvent) {
+ event.preventDefault()
+ event.stopPropagation()
+ clearTimeout(this.debounceTimeoutId)
+ const context = this
+
+ // Resolve the model using a debounce to avoid unnecessary sequential requests
+ // Make sure that the changes in the input are debounced
+ this.debounceTimeoutId = setTimeout(function () {
+ if (context.localModel.nameIsNumeric()) {
+ let latLng = context.localModel.getLatLng()
+ context.model.setLngLat(latLng.lng, latLng.lat)
+ }
+ if (event.key === 'Enter') {
+ context.focused = false
+ // We can only try to auto select the first result if the inputted text is not a coordinate
+ if (!context.localModel.nameIsNumeric()) {
+ if (appConfig.autoSelectFirstExactAddressMatchOnSearchEnter) {
+ EventBus.$emit('showLoading', true)
+ PlacesSearch(context.localModel.placeName, 10).then(places => {
+ // If the first result is an address and the match_type is exact, then we auto select the first item on the enter/return action
+ const addresses = context.lodash.filter(places, (p) => {
+ return (p.properties.layer === 'address' || p.properties.layer === 'postalcode') && p.properties.match_type === 'exact'
+ })
+
+ if (addresses.length === 1) {
+ context.selectSuggestion(addresses[0])
+ } else { // if not call the search handler
+ context.autocompleteSearch()
+ }
+ }).catch(response => {
+ console.log(response)
+ // In case of any fail, call the search mode handler
+ context.autocompleteSearch()
+ }).finally(() => {
+ EventBus.$emit('showLoading', false)
+ })
+ } else {
+ context.autocompleteSearch()
+ }
+ } else { // If a coordinate was inputted, call the auto complete
+ context.autocompleteSearch()
+ }
+ } else {
+ context.autocompleteSearch()
+ }
+ }, 1000)
+ }
+ }
+ },
+ // Search for a place based on the place input value
+ autocompleteSearch () {
+ // Make sure that the local model is up-to-date
+ if (!this.localModel || this.localModel.placeName.length === 0) {
+ this.localModel = this.model.clone()
+ }
+ if (this.localModel.nameIsNumeric()) {
+ const latLng = this.model.getLatLng()
+ EventBus.$emit('showLoading', true)
+ const context = this
+ ReverseGeocode(latLng.lat, latLng.lng, 10).then(places => {
+ const place = new Place(latLng.lng, latLng.lat)
+ place.setSuggestions(places)
+ context.localModel = place
+ context.focused = true
+ this.focusIsAutomatic = false
+ if (places.length > 1) {
+ Utils.hideMobileKeyboard()
+ }
+ context.$emit('autocompleted')
+ }).catch(response => {
+ console.log(response)
+ }).finally(() => {
+ context.searching = false
+ EventBus.$emit('showLoading', false)
+ })
+ } else {
+ this.searching = true
+ if (!this.localModel.placeName || this.model.placeName.length === 0) {
+ this.localModel = new Place()
+ this.searching = false
+ } else {
+ const context = this
+ // Run the place search
+ EventBus.$emit('showLoading', true)
+ PlacesSearch(this.localModel.placeName, 10).then(places => {
+ context.localModel.setSuggestions(places)
+ context.focused = true
+ this.focusIsAutomatic = false
+ if (places.length === 0) {
+ context.showInfo(context.$t('placeInput.noPlaceFound'))
+ } else if (places.length > 1) {
+ Utils.hideMobileKeyboard()
+ }
+ context.$emit('autocompleted')
+ }).catch(response => {
+ console.log(response)
+ context.showError(context.$t('placeInput.unknownSearchPlaceError'))
+ }).finally(() => {
+ context.searching = false
+ EventBus.$emit('showLoading', false)
+ })
+ }
+ }
+ },
+ }
+}
diff --git a/src/fragments/forms/place-input/place-input.js b/src/fragments/forms/place-input/place-input.js
index be1aa1e05..7fe856586 100644
--- a/src/fragments/forms/place-input/place-input.js
+++ b/src/fragments/forms/place-input/place-input.js
@@ -102,8 +102,7 @@ export default {
* @returns {String}
*/
predictableId () {
- let id = `place-input-container-${this.idPostfix}-${this.index}`
- return id
+ return `place-input-container-${this.idPostfix}-${this.index}`
},
/**
* Determines if the automatic focus must be set or not
@@ -122,16 +121,14 @@ export default {
* @returns {Boolean}
*/
isMobile () {
- let isMobile = Utils.isMobile()
- return isMobile
+ return Utils.isMobile()
},
/**
* Determines if the 'pick a place' button must show its tooltip
* @returns {Boolean}
*/
showInputPickPlaceTooltip () {
- let show = this.model.isEmpty() && !this.single && this.$store.getters.isSidebarVisible
- return show
+ return this.model.isEmpty() && !this.single && this.$store.getters.isSidebarVisible
},
/**
* Get the input hint to be displayed
@@ -149,17 +146,7 @@ export default {
* @returns {Boolean}
*/
hideDetails () {
- let hide = this.single || (!this.focused && !this.hasAutomaticFocus)
- return hide
- },
- /**
- * Returns the place input rule required message if it is empty
- * @returns {Boolean|String}
- */
- placeNameRules () {
- return [
- v => !!v || this.$t('placeInput.placeNameRequired')
- ]
+ return this.single || (!this.focused && !this.hasAutomaticFocus)
},
/**
* Return the column breakpoint that must be applied to the input flex
@@ -256,12 +243,10 @@ export default {
},
// Switch the coordinates position ([lat, long] -> [long, lat] and [long, lat] -> [lat, long])
switchCoordsAvailable () {
- const canSwitch = this.model.nameIsNumeric()
- return canSwitch
+ return this.model.nameIsNumeric()
},
searchAvailable () {
- let available = appConfig.supportsSearchMode
- return available
+ return appConfig.supportsSearchMode
},
/**
* Determines if the place input floating menu button is available for the current place input
@@ -304,8 +289,7 @@ export default {
},
showSuggestion () {
- let show = this.focused && !this.focusIsAutomatic
- return show
+ return this.focused && !this.focusIsAutomatic
},
appendBtn () {
if (this.supportSearch) {
@@ -338,9 +322,13 @@ export default {
const regEx = new RegExp(searchMask, 'ig')
let localPlaceName = this.localModel.placeName
let replaceMask
+ //return early if placeName is empty and nothing needs to be highlighted
+ if(localPlaceName === ''){
+ return placeName.trim()
+ }
if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase() + ' ') === 0) {
localPlaceName = localPlaceName[0].toUpperCase() + localPlaceName.substring(1) + ' '
- } else if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase()) === 0 ) {
+ } else if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase()) === 0) {
localPlaceName = localPlaceName[0].toUpperCase() + localPlaceName.substring(1)
} else if ((placeName.toLowerCase()).indexOf(this.localModel.placeName.toLowerCase()) > 0 ) {
localPlaceName = ' ' + localPlaceName[0].toUpperCase() + localPlaceName.substring(1)
@@ -385,8 +373,7 @@ export default {
}, 1000)
},
showAreaIcon (place) {
- let show = place.properties.layer === 'country' || place.properties.layer === 'region'
- return show
+ return place.properties.layer === 'country' || place.properties.layer === 'region'
},
inputFocused (event) {
event.stopPropagation()
@@ -637,12 +624,14 @@ export default {
this.showError(this.$t('placeInput.pleaseTypeSomething'))
} else {
+ const previousMode = this.$store.getters.mode
if (previousMode === constants.modes.search) {
this.$emit('searchChanged')
- } else {
+ }
+ //TODO: check if the following else is needed (seems like event is not been caught)
+ else {
this.$emit('switchedToSearchMode')
}
- const previousMode = this.$store.getters.mode
this.$store.commit('mode', constants.modes.search)
const appMode = new AppMode(this.$store.getters.mode)
const route = appMode.getRoute([this.localModel])
@@ -870,8 +859,7 @@ export default {
this.$emit('changedDirectPlace', data)
},
getNewGuid (prefix) {
- let guid = Utils.guid(prefix)
- return guid
+ return Utils.guid(prefix)
}
}
}
diff --git a/src/fragments/forms/profile-selector/components/profile-selector-option/ProfileSelectorOption.vue b/src/fragments/forms/profile-selector/components/profile-selector-option/ProfileSelectorOption.vue
index 5b277c360..047e587ad 100644
--- a/src/fragments/forms/profile-selector/components/profile-selector-option/ProfileSelectorOption.vue
+++ b/src/fragments/forms/profile-selector/components/profile-selector-option/ProfileSelectorOption.vue
@@ -1,12 +1,12 @@
-
+
{{profile.icon}}
-
-
diff --git a/src/fragments/map-view/components/map-right-click/i18n/map-right-click.i18n.en-us.js b/src/fragments/map-view/components/map-right-click/i18n/map-right-click.i18n.en-us.js
index 8febe3ced..d0d56fa76 100755
--- a/src/fragments/map-view/components/map-right-click/i18n/map-right-click.i18n.en-us.js
+++ b/src/fragments/map-view/components/map-right-click/i18n/map-right-click.i18n.en-us.js
@@ -8,6 +8,8 @@ export default {
addRouteStop: 'Add stop here',
centerHere: 'Center here',
addAsIsochroneCenter: 'Add as reach center',
- inspectDataOnOSM: 'Inspect data on OSM'
+ inspectDataOnOSM: 'Inspect data on OSM',
+ addJob: 'Add Job',
+ addVehicle: 'Add Vehicle'
}
}
diff --git a/src/fragments/map-view/components/map-right-click/map-right-click.js b/src/fragments/map-view/components/map-right-click/map-right-click.js
index 28e023079..7e8f9a920 100644
--- a/src/fragments/map-view/components/map-right-click/map-right-click.js
+++ b/src/fragments/map-view/components/map-right-click/map-right-click.js
@@ -30,7 +30,7 @@ export default {
},
computed: {
canAddStop () {
- return this.$store.getters.mode === constants.modes.directions && this.mapViewData.hasRoutes()
+ return this.$store.getters.mode === constants.modes.directions && this.mapViewData.hasRoutes() && this.mapViewData.places.length < appConfig.maxPlaceInputs
},
show () {
return this.showRightClickPopup
@@ -48,7 +48,13 @@ export default {
return this.$store.getters.mode === constants.modes.isochrones || (this.$store.getters.mode === constants.modes.place && !this.$store.getters.isSidebarVisible)
},
canRoute () {
- return this.$store.getters.mode !== constants.modes.isochrones
+ return this.$store.getters.mode === constants.modes.directions || this.$store.getters.mode === constants.modes.place
+ },
+ canAddJob () {
+ return this.$store.getters.mode === constants.modes.optimization || (this.$store.getters.mode === constants.modes.place && !this.$store.getters.isSidebarVisible)
+ },
+ canAddVehicle () {
+ return this.$store.getters.mode === constants.modes.optimization || (this.$store.getters.mode === constants.modes.place && !this.$store.getters.isSidebarVisible)
},
canShowInspector () {
return true
diff --git a/src/fragments/map-view/components/map-view-markers/MapViewMarkers.vue b/src/fragments/map-view/components/map-view-markers/MapViewMarkers.vue
index d2fbf2248..80706587a 100644
--- a/src/fragments/map-view/components/map-view-markers/MapViewMarkers.vue
+++ b/src/fragments/map-view/components/map-view-markers/MapViewMarkers.vue
@@ -9,7 +9,7 @@
{{marker.label}}
+ @click="removePlace(marker, index)">
delete
{{marker.label}}
+
+
+
+ {{ $t(`optimization.${i}`) }}: {{ j }}
+ {{ $t(`optimization.${i}`) }}: {{ j[0] }}
+
+ {{ $t(`optimization.${i}`) }}: {{ skillIds(j) }}
+
+
+
+
+
+ {{ $t(`optimization.${i}`) }}: {{ v[0] }} - {{ v[1] }}
+ {{ $t(`optimization.${i}`) }}: {{ v[0] }}
+ {{ $t(`optimization.${i}`) }}: {{ skillIds(v) }}
+ {{i}}: {{v}}
+
+
+
delete
+
+ edit
+
settings_ethernet
diff --git a/src/fragments/map-view/components/map-view-markers/map-view-markers.js b/src/fragments/map-view/components/map-view-markers/map-view-markers.js
index d0667f12b..e71994b91 100644
--- a/src/fragments/map-view/components/map-view-markers/map-view-markers.js
+++ b/src/fragments/map-view/components/map-view-markers/map-view-markers.js
@@ -2,6 +2,7 @@ import Vue2LeafletMarkerCluster from 'vue2-leaflet-markercluster'
import constants from '@/resources/constants'
import {LMarker, LPopup} from 'vue2-leaflet'
import appConfig from '@/config/app-config'
+import {EventBus} from '@/common/event-bus'
import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
@@ -58,7 +59,7 @@ export default {
if (this.isPoi || this.$store.getters.embed) {
return false
}
- let markerRemovableModes = [constants.modes.directions, constants.modes.roundTrip, constants.modes.isochrones, constants.modes.place]
+ let markerRemovableModes = [constants.modes.directions, constants.modes.roundTrip, constants.modes.isochrones, constants.modes.place, constants.modes.optimization]
let isRemovable = markerRemovableModes.includes(this.mode)
return isRemovable
},
@@ -71,11 +72,17 @@ export default {
if (this.isPoi || this.$store.getters.embed) {
return false
}
- const draggableModes = [constants.modes.directions, constants.modes.roundTrip, constants.modes.isochrones]
+ const draggableModes = [constants.modes.directions, constants.modes.roundTrip, constants.modes.isochrones, constants.modes.optimization]
const isDraggable = draggableModes.includes(this.mode)
return isDraggable
},
+ modeIsOptimization () {
+ if (this.mode === constants.modes.optimization) {
+ return true
+ }
+ },
+
/**
* Show the marker popup
*/
@@ -124,8 +131,27 @@ export default {
removePlace(index) {
this.$emit('removePlace', index)
},
+ editPlace(index) {
+ if (this.markers[index].job) {
+ EventBus.$emit('editJob', this.markers[index].job.id)
+ } else if (this.markers[index].vehicle) {
+ EventBus.$emit('editVehicle', this.markers[index].vehicle.id)
+ }
+ },
markAsDirectFromHere (index) {
this.$emit('markAsDirectFromHere', index)
+ },
+
+ skillIds(skills) {
+ let ids = ''
+ for (const skill of skills) {
+ if(ids === ''){
+ ids = skill.id
+ } else {
+ ids = ids + ', ' + skill.id
+ }
+ }
+ return ids
}
},
}
diff --git a/src/fragments/map-view/i18n/map-view.i18n.en-us.js b/src/fragments/map-view/i18n/map-view.i18n.en-us.js
index a22878c33..0623e112e 100644
--- a/src/fragments/map-view/i18n/map-view.i18n.en-us.js
+++ b/src/fragments/map-view/i18n/map-view.i18n.en-us.js
@@ -53,6 +53,7 @@ export default {
yourLocation: 'Use your location',
setMyLocationAsMapCenter: 'Do you want to center the map at your current location? This will improve place search precision. You will have to authorize it if prompted.',
removePlace: 'Remove place',
+ editDetails: 'Edit details',
viewOnORS: 'View on ORS',
moveMapPositionToLeft: 'Move map center to the left',
moveMapPositionToRight: 'Move map center to the right',
diff --git a/src/fragments/map-view/map-view.js b/src/fragments/map-view/map-view.js
index 22520415f..bbf5897e2 100644
--- a/src/fragments/map-view/map-view.js
+++ b/src/fragments/map-view/map-view.js
@@ -168,7 +168,6 @@ export default {
initialMaxZoom: appConfig.initialMapMaxZoom,
localMapViewData: new MapViewData(), // we use a local copy of the mapViewData to be able to modify it
mainRouteColor: theme.primary,
- alternativeRouteColor: constants.alternativeRouteColor,
routeBackgroundColor: constants.routeBackgroundColor,
guid: null,
clickLatLng: null,
@@ -193,6 +192,9 @@ export default {
}
},
computed: {
+ theme() {
+ return theme
+ },
showMyLocationControl () {
return this.supportsMyLocationBtn && !this.isAltitudeModalOpen && this.showControls
@@ -378,6 +380,9 @@ export default {
let markers = GeoUtils.buildMarkers(markersMapViewData.places, isRoute, this.focusedPlace)
markers = this.$root.appHooks.run('markersCreated', markers)
return markers
+ } else if (markersMapViewData.jobs.length || markersMapViewData.vehicles.length) {
+ const unassignedJobs = this.localMapViewData.rawData !== null ? this.localMapViewData.rawData.unassigned : []
+ return GeoUtils.buildOptimizationMarkers(markersMapViewData.jobs, markersMapViewData.vehicles, unassignedJobs)
}
},
/**
@@ -443,13 +448,6 @@ export default {
return markerData
}
},
- /**
- * Determines if a route stop can be added
- */
- canAddStop () {
- const can = !Array.isArray(this.markers) || this.markers.length < appConfig.maxPlaceInputs
- return can
- },
/**
* Return the current map polyline measures options
* @returns {Object} options
@@ -679,6 +677,12 @@ export default {
}, 500)
},
+ alternativeRouteColor(route) {
+ if(this.mode === constants.modes.optimization) {
+ return constants.vehicleColors[route.vehicle]
+ }
+ return constants.alternativeRouteColor
+ },
/**
* Refresh the altitude modal (force a 'destroy' and a 'rebuild')
* with the new data
@@ -887,6 +891,11 @@ export default {
}
}
},
+ updateOnlyMarkers (data) {
+ this.localMapViewData.jobs = data.jobs
+ this.localMapViewData.vehicles = data.vehicles
+ this.localMapViewData.routes = []
+ },
/**
* Handles the marker move
* by creating a debounce-timeout in order to
@@ -900,7 +909,7 @@ export default {
// Only marker changes that are a result of user interaction are treated here.
// With vue2-leaflet version 2.5.2 the event.originalEvent is not an instance of
// window.PointerEvent anymore and use parent window.MouseEvent instead
- if (event.originalEvent instanceof window.MouseEvent || event.originalEvent instanceof window.TouchEvent) {
+ if (event.originalEvent?.type === 'mousemove') {
clearTimeout(this.markerMoveTimeoutId)
this.markerMoveTimeoutId = setTimeout(() => {
this.markerDragEnd(event)
@@ -933,7 +942,9 @@ export default {
removePlace (markerIndex) {
if (this.markers[markerIndex]) {
let place = this.markers[markerIndex].place
- let data = {place, index: markerIndex}
+ let job = this.markers[markerIndex].job
+ let vehicle = this.markers[markerIndex].vehicle
+ let data = {place, job, vehicle, index: markerIndex}
this.$emit('removePlace', data)
}
},
@@ -977,6 +988,7 @@ export default {
if (markerIndex !== null) {
const marker = this.markers[markerIndex]
marker.inputIndex = markerIndex
+ marker.text = event.originalEvent.target.innerText
this.$emit('markerDragged', marker)
}
},
@@ -1062,11 +1074,11 @@ export default {
*
*/
loadMapData () {
- if (this.localMapViewData.hasPlaces()) {
+ if (this.localMapViewData.hasPlaces() || this.localMapViewData.jobs.length || this.localMapViewData.vehicles.length) {
this.defineActiveRouteIndex()
this.updateMarkersLabel()
if (this.hasOnlyOneMarker && this.fitBounds) {
- this.setFocusedPlace(this.localMapViewData.places[0])
+ this.setFocusedPlace(this.localMapViewData.places[0] || this.localMapViewData.jobs[0] || this.localMapViewData.vehicles[0])
}
if (this.mode === constants.modes.place && this.hasOnlyOneMarker && appConfig.showAdminAreaPolygon) {
this.loadAdminArea()
@@ -1081,10 +1093,12 @@ export default {
* @param {Place} place
*/
setFocusedPlace (place) {
- let layer = place.layer || place.properties.layer
- if (layer) {
+ if (place.layer || place.properties.layer) {
+ let layer = place.layer || place.properties.layer
this.zoomLevel = GeoUtils.zoomLevelByLayer(layer)
this.setMapCenter(place.getLatLng())
+ } else {
+ this.setMapCenter(place.getLatLng())
}
},
/**
@@ -1134,6 +1148,8 @@ export default {
if (this.localMapViewData.hasPlaces() || polylineData.length > 0) {
let places = Place.getFilledPlaces(this.localMapViewData.places)
this.dataBounds = GeoUtils.getBounds(places, polylineData)
+ } else if (this.localMapViewData.jobs.length || this.localMapViewData.vehicles.length) {
+ this.dataBounds = GeoUtils.getBounds([], polylineData)
} else {
this.dataBounds = null
}
@@ -1235,8 +1251,8 @@ export default {
* @param pickEditSource
* @emits setInputPlace
*/
- setInputPlace (placeIndex, placeInputId, place) {
- let data = {pickPlaceIndex: placeIndex, placeInputId: placeInputId, place: place}
+ setInputPlace (placeIndex, placeInputId, place, pickEditSource) {
+ let data = {pickPlaceIndex: placeIndex, placeInputId: placeInputId, place: place, pickEditSource: pickEditSource}
this.$emit('setInputPlace', data)
},
@@ -1344,7 +1360,7 @@ export default {
if (!insidePolygon) {
const mapEl = this.$refs.map.$el
GeoUtils.normalizeCoordinates(event.latlng)
- const data = { event, mapEl, canAddStop: this.canAddStop }
+ const data = { event, mapEl }
// Event to be caught by the MapRightClick.vue component
EventBus.$emit('mapRightClicked', data)
}
@@ -1426,12 +1442,14 @@ export default {
let context = this
let pickPlaceIndex = context.$store.getters.pickPlaceIndex
let pickPlaceId = context.$store.getters.pickPlaceId
+ let pickEditSource = context.$store.getters.pickEditSource || null
place.resolve().then(() => {
- context.setInputPlace(pickPlaceIndex, pickPlaceId, place)
+ context.setInputPlace(pickPlaceIndex, pickPlaceId, place, pickEditSource)
// Once a place was picked up,
// remove the store pick place data
context.$store.commit('pickPlaceIndex', null)
context.$store.commit('pickPlaceId', null)
+ context.$store.commit('pickEditSource', null)
})
},
@@ -1954,6 +1972,12 @@ export default {
context.adjustMap()
}
})
+
+ EventBus.$on('updateOnlyMarkers', (data) => {
+ if (data.jobs || data.vehicles) {
+ context.updateOnlyMarkers(data)
+ }
+ })
},
/**
diff --git a/src/fragments/sidebar/Sidebar.vue b/src/fragments/sidebar/Sidebar.vue
index cfe38305a..bb46f384a 100755
--- a/src/fragments/sidebar/Sidebar.vue
+++ b/src/fragments/sidebar/Sidebar.vue
@@ -11,9 +11,10 @@
disable-resize-watcher
:width="$mdAndUpResolution ? $store.getters.sidebarFullWidth : $store.getters.sidebarShrunkWidth"
:permanent="$store.getters.leftSideBarPinned"
- :class="{'auto-height': $lowResolution && !$store.getters.leftSideBarPinned, 'full-height': $store.getters.leftSideBarPinned}">
+ :class="{'auto-height': $lowResolution && !$store.getters.leftSideBarPinned, 'full-height': $store.getters.leftSideBarPinned}"
+ data-cy="sidebar">
-