412 lines
15 KiB
Vue
412 lines
15 KiB
Vue
<script>
|
|
import Layout from '@layouts/main.vue'
|
|
import { mapState } from 'vuex'
|
|
import axios from 'axios'
|
|
import Papa from 'papaparse'
|
|
|
|
export default {
|
|
page: {
|
|
title: 'Generic Import',
|
|
meta: [{ name: 'description', content: 'The Generic Import page.' }],
|
|
},
|
|
components: { Layout },
|
|
props: {
|
|
user: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
data: function () {
|
|
return {
|
|
file: null,
|
|
tryingToCreate: false,
|
|
errors: [],
|
|
papaConfig: { dynamicTyping: true, skipEmptyLines: true, complete: this.assignResults },
|
|
fileData: null,
|
|
fileHeadings: null,
|
|
myVehicles: [],
|
|
selectedVehicle: null,
|
|
invertFullTank: null,
|
|
filledValueString: '',
|
|
notFilledValueString: '',
|
|
isFullTankString: false,
|
|
fileHeadingMap: {
|
|
fuelQuantity: null,
|
|
perUnitPrice: null,
|
|
totalAmount: null,
|
|
odoReading: null,
|
|
isTankFull: null,
|
|
hasMissedFillup: null,
|
|
comments: [], // [int]
|
|
fillingStation: null,
|
|
date: null,
|
|
fuelSubType: null,
|
|
},
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState('utils', ['isMobile']),
|
|
...mapState('vehicles', ['vehicles']),
|
|
uploadButtonLabel() {
|
|
if (this.isMobile) {
|
|
if (this.file == null) {
|
|
return this.$t('choosephoto')
|
|
} else {
|
|
return ''
|
|
}
|
|
} else {
|
|
if (this.file == null) {
|
|
return this.$t('choosefile')
|
|
} else {
|
|
return ''
|
|
}
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
this.myVehicles = this.vehicles
|
|
},
|
|
methods: {
|
|
assignResults(results, file) {
|
|
this.fileData = results.data
|
|
this.fileHeadings = results.data[0]
|
|
},
|
|
parseCSV() {
|
|
if (this.file == null) {
|
|
return
|
|
}
|
|
this.errorMessage = ''
|
|
Papa.parse(this.file, this.papaConfig)
|
|
},
|
|
getUsedHeadings() {
|
|
return Object.keys(this.fileHeadingMap).filter((k) => this.fileHeadingMap[k] != null) // filter non-null properties
|
|
},
|
|
getTimezone() {
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
},
|
|
csvToJson() {
|
|
const data = []
|
|
const headings = this.getUsedHeadings().reduce((a, k) => ({ ...a, [k]: this.fileHeadingMap[k] }), {}) // create new object from filter
|
|
const comments = (row) => {
|
|
return this.fileHeadingMap.comments.reduce((a, fi) => {
|
|
// TODO: sanitize to prevent XSS
|
|
return `${a}${this.fileHeadings[fi]}: ${row[fi]}\n`
|
|
}, '')
|
|
}
|
|
const calculateTotal = (row) => {
|
|
return this.fileHeadingMap.totalAmount === -1
|
|
? row[this.fileHeadingMap.fuelQuantity] * row[this.fileHeadingMap.perUnitPrice]
|
|
: row[this.fileHeadingMap.totalAmount]
|
|
}
|
|
|
|
const setFullTank = (row) => {
|
|
if (row[this.fileHeadingMap.isTankFull].toLowerCase() === this.filledValueString.toLowerCase()) {
|
|
return true
|
|
} else if (row[this.fileHeadingMap.isTankFull].toLowerCase() === this.notFilledValueString.toLowerCase()) {
|
|
return false
|
|
} else {
|
|
// TODO: need to handle errors better
|
|
throw Error
|
|
}
|
|
}
|
|
|
|
for (let r = 1; r < this.fileData.length; r++) {
|
|
const row = this.fileData[r]
|
|
const item = {}
|
|
Object.keys(headings).forEach((k) => {
|
|
if (k === 'comments') {
|
|
item[k] = comments(row)
|
|
} else if (k === 'totalAmount') {
|
|
item[k] = calculateTotal(row)
|
|
} else if (k === 'isTankFull') {
|
|
if (this.isFullTankString) {
|
|
item[k] = setFullTank(row)
|
|
} else {
|
|
if (this.invertFullTank) {
|
|
item[k] = Boolean(!row[headings[k]])
|
|
} else {
|
|
item[k] = Boolean(row[headings[k]])
|
|
}
|
|
}
|
|
} else if (k === 'hasMissedFillup') {
|
|
// TODO: need to account for this field being a string
|
|
item[k] = Boolean(row[headings[k]])
|
|
} else if (k === 'date') {
|
|
item[k] = new Date(row[headings[k]]).toISOString()
|
|
} else {
|
|
item[k] = row[headings[k]]
|
|
}
|
|
})
|
|
data.push(item)
|
|
}
|
|
return data
|
|
},
|
|
importData() {
|
|
if (this.errors.length === 0) {
|
|
try {
|
|
const content = {
|
|
data: this.csvToJson(),
|
|
vehicleId: this.selectedVehicle.id,
|
|
timezone: this.getTimezone(),
|
|
}
|
|
axios
|
|
.post('/api/import/generic', content)
|
|
.then((data) => {
|
|
this.$buefy.toast.open({
|
|
message: this.$t('importsuccessfull'),
|
|
type: 'is-success',
|
|
duration: 3000,
|
|
})
|
|
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
|
|
})
|
|
.catch((ex) => {
|
|
this.$buefy.toast.open({
|
|
duration: 5000,
|
|
message: this.$t('importerror'),
|
|
position: 'is-bottom',
|
|
type: 'is-danger',
|
|
})
|
|
console.log(ex)
|
|
if (ex.response && ex.response.data.error) {
|
|
this.errors.push(ex.response.data.error)
|
|
}
|
|
})
|
|
} catch (e) {
|
|
// TODO: handle error
|
|
this.errors.push(e)
|
|
}
|
|
} else {
|
|
this.errors.push('fix errors')
|
|
}
|
|
},
|
|
checkFieldString() {
|
|
const tankFull = this.fileData[1][this.fileHeadingMap.isTankFull]
|
|
if (typeof tankFull !== 'boolean' && typeof tankFull === 'string') {
|
|
this.isFullTankString = true
|
|
}
|
|
},
|
|
clearHeadingProperty(property) {
|
|
if (property === 'comments') {
|
|
this.fileHeadingMap[property] = []
|
|
} else {
|
|
this.fileHeadingMap[property] = null
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Layout>
|
|
<div class="columns box">
|
|
<div class="column">
|
|
<h1 class="title">{{ $t('importgeneric') }}</h1>
|
|
</div>
|
|
</div>
|
|
<br />
|
|
<div v-if="fileData === null" class="columns">
|
|
<div class="column">
|
|
<p class="subtitle"> {{ $t('stepstoimport', { name: 'CSV' }) }}</p>
|
|
<ol>
|
|
<!-- <li>{{ $t('importhintcreatecsv', { 'name': 'Fuelly' }) }} <a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">{{ $t('here') }}</a>.</li> -->
|
|
<li>{{ $t('importgenerichintdata') }}</li>
|
|
<li>{{ $t('importhintvehiclecreated') }}</li>
|
|
<li v-html="$t('importhintcurrdist')"></li>
|
|
<li v-html="$t('importhintunits')"></li>
|
|
<li>
|
|
<b>{{ $t('dontimportagain') }}</b>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
<div v-if="fileData === null" class="section box">
|
|
<div class="columns">
|
|
<div class="column is-two-thirds">
|
|
<p class="subtitle">{{ $t('choosedatafile') }}</p>
|
|
</div>
|
|
<div class="column is-one-third is-flex is-align-content-center">
|
|
<form @submit.prevent="parseCSV">
|
|
<div class="columns">
|
|
<div class="column">
|
|
<b-field class="file is-primary" :class="{ 'has-name': !!file }">
|
|
<b-upload v-model="file" class="file-label" accept=".csv" required>
|
|
<span class="file-cta">
|
|
<b-icon class="file-icon" icon="upload"></b-icon>
|
|
<span class="file-label">{{ uploadButtonLabel }}</span>
|
|
</span>
|
|
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
|
|
{{ file.name }}
|
|
</span>
|
|
</b-upload>
|
|
</b-field>
|
|
</div>
|
|
<div class="column">
|
|
<b-button tag="button" native-type="submit" type="is-primary" class="control">
|
|
{{ $t('import') }}
|
|
</b-button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="columns">
|
|
<div class="column">
|
|
<p class="subtitle">Map Fields</p>
|
|
<form class="" @submit.prevent="importData">
|
|
<b-field :label="$t('selectvehicle')">
|
|
<b-select v-model="selectedVehicle" :placeholder="$t('vehicle')" required expanded>
|
|
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
|
{{ option.nickname }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<span v-if="selectedVehicle !== null">
|
|
<b-field :label="$t('fillupdate')">
|
|
<b-select v-model="fileHeadingMap.date" required expanded>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field>
|
|
<template v-slot:label>
|
|
{{ $t('fuelsubtype') }}
|
|
<b-tooltip type="is-dark" label="Clear selection">
|
|
<b-button
|
|
type="is-ghost"
|
|
size="is-small"
|
|
icon-pack="fas"
|
|
icon-right="arrow-rotate-left"
|
|
@click="clearHeadingProperty('fuelSubType')"
|
|
></b-button>
|
|
</b-tooltip>
|
|
</template>
|
|
<b-select v-model="fileHeadingMap.fuelSubType" expanded>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field :label="$t('quantity')">
|
|
<b-select v-model="fileHeadingMap.fuelQuantity" expanded required>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field :label="$t('per', { '0': $t('price'), '1': $t('unit.short.' + selectedVehicle.fuelUnitDetail.key) })">
|
|
<b-select v-model.number="fileHeadingMap.perUnitPrice" type="number" min="0" step=".001" expanded required>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field :label="$t('totalamountpaid')">
|
|
<b-select v-model.number="fileHeadingMap.totalAmount" expanded required>
|
|
<option value="-1">Calculated</option>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field :label="$t('odometer')">
|
|
<b-select v-model.number="fileHeadingMap.odoReading" expanded required>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field :label="$t('tankpartialfull')">
|
|
<b-radio-button v-model="invertFullTank" native-value="false">{{ $t('fulltank') }}</b-radio-button>
|
|
<b-radio-button v-model="invertFullTank" native-value="true">{{ $t('partialfillup') }}</b-radio-button>
|
|
</b-field>
|
|
<b-field>
|
|
<b-select v-model="fileHeadingMap.isTankFull" required @input="checkFieldString">
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<span v-if="isFullTankString === true" required>
|
|
<b-field label="Value when tank is filled">
|
|
<b-input v-model="filledValueString"></b-input>
|
|
</b-field>
|
|
<b-field label="Value when tank was not completely filled">
|
|
<b-input v-model="notFilledValueString"></b-input>
|
|
</b-field>
|
|
</span>
|
|
<b-field>
|
|
<template v-slot:label>
|
|
{{ $t('missedfillup') }}
|
|
<b-tooltip type="is-dark" label="Clear selection">
|
|
<b-button
|
|
type="is-ghost"
|
|
size="is-small"
|
|
icon-pack="fas"
|
|
icon-right="arrow-rotate-left"
|
|
@click="clearHeadingProperty('hasMissedFillup')"
|
|
></b-button>
|
|
</b-tooltip>
|
|
</template>
|
|
<b-select v-model="fileHeadingMap.hasMissedFillup">
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field>
|
|
<template v-slot:label>
|
|
{{ $t('fillingstation') }}
|
|
<b-tooltip type="is-dark" label="Clear selection">
|
|
<b-button
|
|
type="is-ghost"
|
|
size="is-small"
|
|
icon-pack="fas"
|
|
icon-right="arrow-rotate-left"
|
|
@click="clearHeadingProperty('fillingStation')"
|
|
></b-button>
|
|
</b-tooltip>
|
|
</template>
|
|
<b-select v-model="fileHeadingMap.fillingStation">
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<b-field>
|
|
<template v-slot:label>
|
|
{{ $t('comments') }}
|
|
<b-tooltip type="is-dark" label="Clear selection">
|
|
<b-button
|
|
type="is-ghost"
|
|
size="is-small"
|
|
icon-pack="fas"
|
|
icon-right="arrow-rotate-left"
|
|
@click="clearHeadingProperty('comments')"
|
|
></b-button>
|
|
</b-tooltip>
|
|
</template>
|
|
<b-select v-model="fileHeadingMap.comments" type="textarea" multiple expanded>
|
|
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
|
{{ option }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<br />
|
|
<b-field>
|
|
<b-button tag="button" native-type="submit" type="is-primary" :value="$t('save')" :label="$t('import')" expanded />
|
|
<p v-if="authError"> There was an error logging in to your account. </p>
|
|
</b-field>
|
|
</span>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<b-message v-if="errors.length" type="is-danger">
|
|
<ul>
|
|
<li v-for="error in errors" :key="error">{{ error }}</li>
|
|
</ul>
|
|
</b-message>
|
|
</Layout>
|
|
</template>
|