837 lines
39 KiB
JavaScript
837 lines
39 KiB
JavaScript
Vue.component('warehouse-shipping-note-modal-text-elements', {
|
|
props: {
|
|
textElements: Array
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
textElementsData: [],
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div style="display: flex; align-items: center; justify-content: center;">
|
|
<template v-if="textElementsData.length > 0">
|
|
<div v-for="textElement in textElementsData" style="display: inline-block; margin-right: 10px;">
|
|
<input type="checkbox" v-model="textElements[textElement.id]" :id="'textElement' + textElement.id">
|
|
<label :for="'textElement' + textElement.id" :title="textElement.content">{{ textElement.title }}</label>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="text-center">
|
|
<i class="fa fa-spinner fa-spin"></i>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
`,
|
|
async mounted() {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getAllTextElements');
|
|
this.textElementsData = response.data;
|
|
}
|
|
})
|
|
|
|
// TODO: maybe also think about creating a component for simple forms like this
|
|
Vue.component('warehouse-shipping-note-modal-hours-entry', {
|
|
props: {
|
|
index: {type: [Number], required: false, default: null},
|
|
showHourlyPrice: {type: Boolean, default: false},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
userApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/userAutoComplete',
|
|
carApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/timerecordingCarAutoComplete',
|
|
userId: '',
|
|
carId: '',
|
|
date: '',
|
|
hourCount: '',
|
|
kilometerCount: '',
|
|
hourlyPrice: '',
|
|
priceType: 'normal',
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div class="warehouse-shipping-note-modal-hours-entry-container" v-bind:class="{ 'hideHourlyPrice': !showHourlyPrice, 'hideKilometer': window.TT_CONFIG['WAREHOUSE_ADMIN'] != true }">
|
|
<tt-autocomplete v-model="userId" :api-url="userApiUrl" label="Mitarbeiter" sm/>
|
|
<tt-input v-model="date" label="Datum" type="date" sm/>
|
|
<tt-input v-model="hourCount" label="Stunden" sm/>
|
|
<tt-select v-model="priceType" label="Stundenart" sm :options="[{text: 'Normal', value: 'normal'}, {text: '+50%', value: '50'}, {text: '+100%', value: '100'}]"/>
|
|
<tt-autocomplete v-model="carId" :api-url="carApiUrl" label="Fahrzeug" sm/>
|
|
<tt-input v-model="hourlyPrice" label="Stundenlohn" type="number" sm v-if="showHourlyPrice"/>
|
|
<tt-input v-show="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true" :disabled="carId === ''" v-model="kilometerCount" label="Kilometer" sm/>
|
|
<div class="warehouse-shipping-note-modal-hours-entry-actions">
|
|
<button @click="createOrUpdate" class="btn btn-sm btn-primary">Speichern</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
async createOrUpdate() {
|
|
if (!this.userId || !this.date || !this.hourCount) {
|
|
this.window.notify('error', 'Bitte füllen Sie alle Felder aus');
|
|
return;
|
|
}
|
|
|
|
this.$emit(this.index === null ? 'create' : 'update', {
|
|
userId: this.userId,
|
|
date: this.date,
|
|
hourCount: this.hourCount,
|
|
priceType: this.priceType,
|
|
hourlyPrice: this.hourlyPrice || null,
|
|
carId: this.carId ? this.carId : null,
|
|
kilometerCount: this.carId ? this.kilometerCount : null
|
|
});
|
|
// TODO: maybe make this cleaner
|
|
Object.assign(this.$data, this.$options.data.apply(this))
|
|
await this.$nextTick();
|
|
this.userId = this.window.TT_CONFIG['USER_ID']
|
|
this.updateDate();
|
|
this.updateKilometerCount().then();
|
|
this.updateCarId().then();
|
|
},
|
|
async updateKilometerCount() {
|
|
if (!this.carId) {
|
|
this.kilometerCount = '';
|
|
return;
|
|
}
|
|
const delAddr = this.$parent.$parent.$parent.delAddrLine +
|
|
' ' +
|
|
this.$parent.$parent.$parent.delAddrCity +
|
|
' ' +
|
|
this.$parent.$parent.$parent.delAddrPLZ;
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDistance?from=Xinon%20GmbH&to=' + delAddr);
|
|
this.kilometerCount = response.data.distance
|
|
},
|
|
async updateCarId() {
|
|
if (!this.userId || this.carId) return;
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/timerecordingCarForUser?userId=' + this.userId);
|
|
if (response.data.status === 'USER_NO_CAR') {
|
|
// this.window.notify('info', 'Kein zugewiesenes Fahrzeug gefunden');
|
|
this.carId = '';
|
|
return;
|
|
}
|
|
this.carId = response.data.id;
|
|
},
|
|
updateDate() {
|
|
if (!this.date) {
|
|
const today = new Date();
|
|
const dd = String(today.getDate()).padStart(2, '0');
|
|
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
|
const yyyy = today.getFullYear();
|
|
this.date = `${yyyy}-${mm}-${dd}`;
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
if (!this.userId) this.userId = this.window.TT_CONFIG['USER_ID'];
|
|
if (!this.carId) this.updateCarId().then();
|
|
if (!this.date) this.updateDate();
|
|
if (!this.kilometerCount) this.updateKilometerCount().then();
|
|
|
|
this.$parent.$parent.$parent.$watch('delAddrLine', this.updateKilometerCount);
|
|
this.$watch('carId', this.updateKilometerCount);
|
|
|
|
}
|
|
})
|
|
|
|
// TODO: we should create this to a tt-simple-table component
|
|
Vue.component('warehouse-shipping-note-modal-hours-view', {
|
|
props: {
|
|
hoursEntries: {type: Array, required: true},
|
|
showHourlyPrice: {type: Boolean, default: false},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
userNames: {}
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div style="display: flex; align-items: center; justify-content: center;">
|
|
<table class="table table-striped table-sm" style="width: max-content">
|
|
<thead>
|
|
<tr>
|
|
<th>Mitarbeiter</th>
|
|
<th>Datum</th>
|
|
<th>ST</th>
|
|
<th>KM</th>
|
|
<th v-if="showHourlyPrice">Stundenlohn</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="hoursEntries.length === 0">
|
|
<td colspan="6" class="text-center">Keine Einträge</td>
|
|
</tr>
|
|
<tr v-for="entry in hoursEntries">
|
|
<td>{{ userNames[entry.userId] }}</td>
|
|
<td>{{ window.moment(entry.date).format('DD.MM.YYYY') }}</td>
|
|
<td>{{ entry.hourCount }}</td>
|
|
<td v-show="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">{{ entry.kilometerCount }}</td>
|
|
<td v-if="showHourlyPrice">{{ entry.hourlyPrice }}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-danger" @click="$emit('delete', entry)">Löschen</button>
|
|
<button class="btn btn-sm btn-primary" @click="$emit('edit', entry)">Bearbeiten</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`,
|
|
// add a method and a watcher to fetch the user names
|
|
methods: {
|
|
async fetchUserNames() {
|
|
for (const entry of this.hoursEntries) {
|
|
if (!entry.userId) continue;
|
|
if (entry.userId in this.userNames) continue;
|
|
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/userAutoComplete?searchedID=' + entry.userId);
|
|
this.$set(this.userNames, entry.userId, response.data[0].text);
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
hoursEntries: {
|
|
handler: 'fetchUserNames', immediate: true
|
|
}
|
|
},
|
|
})
|
|
|
|
// this component will combine the above 2 components and show the entries and the input fields
|
|
Vue.component('warehouse-shipping-note-modal-hours', {
|
|
props: {
|
|
hoursEntries: {type: Array, required: true},
|
|
showHourlyPrice: {type: Boolean, default: false},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
selectedUpdateIndex: null,
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div>
|
|
<warehouse-shipping-note-modal-hours-entry @create="create" @update="update" :index.sync="selectedUpdateIndex"
|
|
:show-hourly-price="showHourlyPrice" ref="entry"/>
|
|
<warehouse-shipping-note-modal-hours-view @delete="deleteEntry" @edit="editEntry" :hours-entries="hoursEntries"
|
|
:show-hourly-price="showHourlyPrice"/>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
create(entry) {
|
|
this.$emit('update:hoursEntries', [...this.hoursEntries, entry]);
|
|
this.window.notify('success', 'Eintrag erstellt');
|
|
},
|
|
update(entry) {
|
|
this.$emit('update:hoursEntries', this.hoursEntries.map((oldEntry, index) => index === this.selectedUpdateIndex ? entry : oldEntry));
|
|
this.window.notify('success', 'Eintrag aktualisiert');
|
|
this.selectedUpdateIndex = null;
|
|
},
|
|
deleteEntry(entry) {
|
|
this.$emit('update:hoursEntries', this.hoursEntries.filter(oldEntry => oldEntry !== entry));
|
|
this.window.notify('success', 'Eintrag gelöscht');
|
|
},
|
|
editEntry(entry) {
|
|
this.selectedUpdateIndex = this.hoursEntries.indexOf(entry);
|
|
this.$refs.entry.userId = entry.userId;
|
|
this.$refs.entry.date = entry.date;
|
|
this.$refs.entry.hourCount = entry.hourCount;
|
|
this.$refs.entry.note = entry.note;
|
|
this.$refs.entry.hourlyPrice = entry.hourlyPrice;
|
|
}
|
|
}
|
|
})
|
|
|
|
// now we need the same as above for positions
|
|
// so we need warehouse-shipping-note-modal-positions-entry, warehouse-shipping-note-modal-positions-view and warehouse-shipping-note-modal-positions
|
|
// positions have a article or article packet, amount and price
|
|
// when a article or article packet is selected we should fetch the name and description
|
|
// then fetch the default price for the address
|
|
Vue.component('warehouse-shipping-note-modal-positions-entry', {
|
|
props: {
|
|
index: {type: [Number], required: false, default: null},
|
|
billAddrId: {type: [String, Number], required: true},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
isAdmin: false,
|
|
articleApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle/autoComplete',
|
|
articlePacketApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticlePacket/autoComplete',
|
|
articleId: '',
|
|
articlePacketId: '',
|
|
amount: '',
|
|
price: '',
|
|
isEnergieMaterial: false,
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div class="warehouse-shipping-note-modal-positions-entry-container" :class="{ 'hidePrice': !isAdmin }">
|
|
<tt-autocomplete v-model="articleId" :api-url="articleApiUrl" label="Artikel" sm ref="article"/>
|
|
<!-- <tt-autocomplete v-model="articlePacketId" :api-url="articlePacketApiUrl" label="Artikel Packet" sm/>-->
|
|
<tt-input v-model="amount" label="Menge" sm/>
|
|
<tt-checkbox v-model="isEnergieMaterial" label="Energie Material" sm/>
|
|
<tt-input v-show="isAdmin" v-model="price" label="Preis" type="number" sm/>
|
|
<div class="warehouse-shipping-note-modal-positions-entry-actions">
|
|
<button @click="createOrUpdate" class="btn btn-sm btn-primary">Speichern</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
// TODO: if articlePacket is needed we need to implement this
|
|
async createOrUpdate() {
|
|
if (!this.amount) return this.window.notify('error', 'Bitte füllen sie die Menge aus');
|
|
const data = {
|
|
amount: this.amount,
|
|
isEnergieMaterial: this.isEnergieMaterial,
|
|
price: parseFloat(this.price) ?? ''
|
|
}
|
|
if (isNaN(data.price)) data.price = '';
|
|
if (!this.articleId && this.$refs.article.displayValue) {
|
|
data.articleText = this.$refs.article.displayValue;
|
|
} else if (this.articleId) {
|
|
data.article = this.articleId;
|
|
} else {
|
|
return this.window.notify('error', 'Bitte wählen Sie einen Artikel aus');
|
|
}
|
|
|
|
this.$emit(this.index === null ? 'create' : 'update', data);
|
|
Object.assign(this.$data, this.$options.data.apply(this))
|
|
},
|
|
async fetchPrice() {
|
|
if (!this.articleId && !this.articlePacketId || !this.billAddrId) return;
|
|
|
|
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getArticleAddressPrice?articleId=${this.articleId ||
|
|
this.articlePacketId}&addressId=${this.billAddrId}`;
|
|
const response = await axios.get(url);
|
|
this.price = response.data.price;
|
|
}
|
|
},
|
|
watch: {
|
|
articleId: {handler: 'fetchPrice', immediate: false},
|
|
articlePacketId: {handler: 'fetchPrice', immediate: false},
|
|
billAddrId: {handler: 'fetchPrice', immediate: false},
|
|
},
|
|
})
|
|
|
|
// here will warehouse-shipping-note-modal-positions-view show the positions in a table
|
|
Vue.component('warehouse-shipping-note-modal-positions-view', {
|
|
props: {
|
|
positions: {type: Array, required: true},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
isAdmin: false,
|
|
articleNames: {},
|
|
articlePacketNames: {},
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div style="display: flex; align-items: center; justify-content: center;">
|
|
<table class="table table-striped table-sm" style="width: max-content">
|
|
<thead>
|
|
<tr>
|
|
<th>Artikel</th>
|
|
<th>Menge</th>
|
|
<th>Energie Material</th>
|
|
<th v-if="isAdmin">Preis</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="positions.length === 0">
|
|
<td colspan="4" class="text-center">Keine Einträge</td>
|
|
</tr>
|
|
<tr v-for="position in positions">
|
|
<td>{{ position.article ? articleNames[position.article] : position.articlePacket ? articlePacketNames[position.articlePacket] :
|
|
position.articleText }}
|
|
</td>
|
|
<td>{{ position.amount }}</td>
|
|
<td>{{ position?.isEnergieMaterial ? 'Ja' : 'Nein' }}</td>
|
|
<td v-if="isAdmin">{{ (position.price?.toFixed(2)) }} €</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-danger" @click="$emit('delete', position)">Löschen</button>
|
|
<button class="btn btn-sm btn-primary" @click="$emit('edit', position)">Bearbeiten</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
async fetchNames() {
|
|
// TODO: there must be a better way to do this
|
|
for (const position of this.positions) {
|
|
if (position.article) this.$set(this.articleNames, position.article, 'Loading...');
|
|
if (position.articlePacket) this.$set(this.articlePacketNames, position.articlePacket, 'Loading...');
|
|
}
|
|
|
|
const articlePromises = this.positions.filter(position => position.article)
|
|
.map(position => axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + position.article));
|
|
const articlePacketPromises = this.positions.filter(position => position.articlePacket)
|
|
.map(position => axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticlePacket/autoComplete?searchedID=' + position.articlePacket));
|
|
|
|
const articleResponses = await Promise.all(articlePromises);
|
|
const articlePacketResponses = await Promise.all(articlePacketPromises);
|
|
|
|
for (const response of articleResponses) {
|
|
this.$set(this.articleNames, response.data[0].value, response.data[0].text);
|
|
}
|
|
|
|
for (const response of articlePacketResponses) {
|
|
this.$set(this.articlePacketNames, response.data[0].value, response.data[0].text);
|
|
}
|
|
|
|
}
|
|
},
|
|
// watch positions and fetch article / article packet names - and initially fill them with Loading...
|
|
watch: {
|
|
positions: {
|
|
handler: 'fetchNames', immediate: true
|
|
}
|
|
}
|
|
})
|
|
|
|
// and here we combine the above 2 components
|
|
Vue.component('warehouse-shipping-note-modal-positions', {
|
|
props: {
|
|
positions: {type: Array, required: true},
|
|
billAddrId: {type: [String, Number], required: true},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
articleNames: {},
|
|
articlePacketNames: {},
|
|
selectedUpdateIndex: null,
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div>
|
|
<warehouse-shipping-note-modal-positions-entry @create="create" @update="update" :index.sync="selectedUpdateIndex" :bill-addr-id="billAddrId"
|
|
ref="entry"/>
|
|
<warehouse-shipping-note-modal-positions-view @delete="deleteEntry" @edit="editEntry" :positions="positions"/>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
create(entry) {
|
|
this.$emit('update:positions', [...this.positions, entry]);
|
|
this.window.notify('success', 'Eintrag erstellt');
|
|
},
|
|
update(entry) {
|
|
this.$emit('update:positions', this.positions.map((oldEntry, index) => index === this.selectedUpdateIndex ? entry : oldEntry));
|
|
this.window.notify('success', 'Eintrag aktualisiert');
|
|
this.selectedUpdateIndex = null;
|
|
},
|
|
deleteEntry(entry) {
|
|
this.$emit('update:positions', this.positions.filter(oldEntry => oldEntry !== entry));
|
|
this.window.notify('success', 'Eintrag gelöscht');
|
|
},
|
|
editEntry(entry) {
|
|
this.selectedUpdateIndex = this.positions.indexOf(entry);
|
|
if (entry.article) this.$refs.entry.articleId = entry.article;
|
|
if (entry.articlePacket) this.$refs.entry.articlePacketId = entry.articlePacket;
|
|
if (entry.articleText) this.$refs.entry.$refs.article.displayValue = entry.articleText;
|
|
this.$refs.entry.amount = entry.amount;
|
|
this.$refs.entry.price = entry.price;
|
|
},
|
|
},
|
|
})
|
|
|
|
|
|
// noinspection EqualityComparisonWithCoercionJS
|
|
Vue.component('warehouse-shipping-note-modal', {
|
|
props: {
|
|
id: {type: [String, Number], required: true},
|
|
// available modes are ['sign', 'edit', 'accept', 'create']
|
|
mode: {type: String, default: 'sign'}
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
|
|
billAddrId: '',
|
|
delAddrName: '',
|
|
delAddrLine: '',
|
|
delAddrPLZ: '',
|
|
delAddrCity: '',
|
|
delAddrEMail: '',
|
|
status: '',
|
|
note: '',
|
|
textElements: [],
|
|
hoursEntries: [],
|
|
positions: [],
|
|
}
|
|
},
|
|
|
|
//language=Vue
|
|
template: `
|
|
<tt-modal :show="true" @submit="submit" @delete="reqDelete" :delete="id !== 'create'" :title="title" @update:show="$emit('close')">
|
|
<div style="width: 99%">
|
|
<h4 class="text-center">Liefer- und Rechnungsadresse</h4>
|
|
<tt-autocomplete v-model="billAddrId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
|
|
<warehouse-shipping-note-modal-address :billAddrId="billAddrId" :del-addr-name.sync="delAddrName" :del-addr-line.sync="delAddrLine"
|
|
:del-addr-p-l-z.sync="delAddrPLZ" :del-addr-city.sync="delAddrCity"
|
|
:del-addr-e-mail.sync="delAddrEMail"/>
|
|
|
|
|
|
<div v-show="delAddrFilled === true">
|
|
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && 1 < 0">
|
|
<hr>
|
|
<h4 class="text-center">Textelemente</h4>
|
|
<warehouse-shipping-note-modal-text-elements :text-elements="textElements"/>
|
|
</template>
|
|
|
|
|
|
<hr>
|
|
<tt-textarea label="Art der Arbeit" v-model="note" sm row/>
|
|
|
|
|
|
<hr>
|
|
<h4 class="text-center">Stunden</h4>
|
|
<warehouse-shipping-note-modal-hours :hours-entries.sync="hoursEntries" :show-hourly-price="false"/>
|
|
|
|
|
|
<hr>
|
|
<h4 class="text-center">Positionen</h4>
|
|
<warehouse-shipping-note-modal-positions :positions.sync="positions" :bill-addr-id="billAddrId"/>
|
|
</div>
|
|
|
|
<div v-show="delAddrFilled === false" class="text-center">Bitte füllen Sie die Rechnungs- und Lieferadresse aus</div>
|
|
|
|
</div>
|
|
<!-- TODO: fix these buttons-->
|
|
<template v-slot:footer-prepend v-if="id !== 'create'">
|
|
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'new'" class="btn btn-warning" @click="changeStatus('in_progress')">In
|
|
Bearbeitung
|
|
</button>
|
|
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && (status === 'new' || status === 'in_progress')" class="btn btn-success"
|
|
@click="changeStatus('accepted')">Akzeptieren
|
|
</button>
|
|
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'accepted'" class="btn btn-info" @click="changeStatus('invoiced')">
|
|
Verrechnet
|
|
</button>
|
|
<button class="btn btn-info" @click="$emit('open-signing-modal', id)">Unterschreiben</button>
|
|
</template>
|
|
</tt-modal>
|
|
`,
|
|
|
|
// now we need methods for fetching the shipping note, submiting the shipping note and translate the keys as they are different in the backend
|
|
async mounted() {
|
|
// fetch by /getById?id=ID
|
|
if (this.id !== 'create') {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getById?id=' + this.id);
|
|
this.billAddrId = response.data.billingAddressId;
|
|
this.delAddrName = response.data.deliveryAddressName;
|
|
this.delAddrLine = response.data.deliveryAddressLine;
|
|
this.delAddrPLZ = response.data.deliveryAddressPLZ;
|
|
this.delAddrCity = response.data.deliveryAddressCity;
|
|
this.delAddrEMail = response.data.deliveryAddressEMail;
|
|
this.note = response.data.note;
|
|
this.status = response.data.status;
|
|
|
|
for (const key of ['textElements', 'hoursEntries', 'positions']) {
|
|
try {
|
|
this[key] = JSON.parse(response.data[key]);
|
|
} catch {
|
|
this.textElements = [];
|
|
}
|
|
}
|
|
} else {
|
|
const knr = new URLSearchParams(window.location.search).get('knr');
|
|
if (knr) {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/Api?do=findAddress&fibu_primary_account=1&autocomplete=1&q=' + knr);
|
|
for (const address of response.data) {
|
|
if (address.text.endsWith(`[${knr}]`)) {
|
|
this.billAddrId = address.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
async reqDelete() {
|
|
const response = await axios.post(window.TT_CONFIG['DELETE_URL'], {id: this.id});
|
|
if (response.data.success) {
|
|
this.window.notify('success', response.data.message || 'Erfolgreich gelöscht');
|
|
this.$emit('close');
|
|
} else {
|
|
this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
},
|
|
async changeStatus(newStatus) {
|
|
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/changeStatus', {id: this.id, status: newStatus});
|
|
if (response.data.success) {
|
|
this.window.notify('success', response.data.message || 'Erfolgreich aktualisiert');
|
|
this.status = newStatus;
|
|
this.$emit('close');
|
|
} else {
|
|
this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
},
|
|
async submit() {
|
|
const data = {
|
|
billingAddressId: this.billAddrId,
|
|
deliveryAddressName: this.delAddrName,
|
|
deliveryAddressLine: this.delAddrLine,
|
|
deliveryAddressPLZ: this.delAddrPLZ,
|
|
deliveryAddressCity: this.delAddrCity,
|
|
deliveryAddressEMail: this.delAddrEMail,
|
|
textElements: this.textElements,
|
|
hoursEntries: this.hoursEntries,
|
|
positions: this.positions,
|
|
note: this.note,
|
|
status: this.status ? this.status : 'new'
|
|
}
|
|
|
|
if (this.id !== 'create') data.id = this.id;
|
|
|
|
const url = this.id === 'create' ? window.TT_CONFIG['CREATE_URL'] : window.TT_CONFIG['UPDATE_URL'];
|
|
const response = await axios.post(url, data);
|
|
|
|
if (response.data.success) {
|
|
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
|
|
this.$emit('close');
|
|
} else {
|
|
this.window.notify('error',
|
|
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
computed: {
|
|
title() {
|
|
return this.id === 'create' ? 'Lieferschein erstellen' : `Lieferschein #${this.id} bearbeiten`;
|
|
},
|
|
delAddrFilled() {
|
|
if (this.id !== 'create') return true;
|
|
return !!this.delAddrName && !!this.delAddrLine && !!this.delAddrPLZ && !!this.delAddrCity;
|
|
}
|
|
}
|
|
|
|
})
|
|
|
|
Vue.component('warehouse-shipping-note-modal-address', {
|
|
props: {
|
|
billAddrId: {type: [String, Number], required: true},
|
|
delAddrName: {type: String, required: true},
|
|
delAddrLine: {type: String, required: true},
|
|
delAddrPLZ: {type: String, required: true},
|
|
delAddrCity: {type: String, required: true},
|
|
delAddrEMail: {type: String, required: true},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
addressModes: [{text: 'Wie Rechnungsadresse', value: 'billing'},
|
|
{text: 'Bestehende Lieferadresse', value: 'existing'},
|
|
{text: 'Andere Lieferadresse', value: 'new'}],
|
|
addressMode: 'existing',
|
|
addresses: [],
|
|
fetchedBillAddr: null,
|
|
selectedAddr: '',
|
|
newAddrGeoLatLon: '',
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div>
|
|
<tt-select v-model="addressMode" :options="addressModes" label="Lieferadresse Art" sm row :disabled="billAddrId === ''"/>
|
|
|
|
<template v-if="addressMode === 'existing'">
|
|
<tt-select v-model="selectedAddr" :options="addresses" label="Lieferadresse" sm row/>
|
|
</template>
|
|
|
|
<template v-else-if="addressMode === 'new'">
|
|
<tt-input :value="delAddrName" @input="$emit('update:delAddrName', $event)" label="Lieferadresse Name*" sm row/>
|
|
<tt-input :value="delAddrEMail" @input="$emit('update:delAddrEMail', $event)" label="Lieferadresse E-Mail" sm row/>
|
|
<tt-autocomplete :api-url="window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/geoAutocomplete'" @input="newAddrGeoLatLon = $event"
|
|
label="Adresse*" sm row/>
|
|
|
|
<span v-if="delAddrLine && delAddrPLZ && delAddrCity">Adresse: {{ delAddrLine }}, {{ delAddrPLZ }} {{ delAddrCity }}</span>
|
|
<!-- <tt-input :value="delAddrLine" @input="$emit('update:delAddrLine', $event)" label="Lieferadresse" sm row/>-->
|
|
<!-- <tt-input :value="delAddrPLZ" @input="$emit('update:delAddrPLZ', $event)" label="Lieferadresse PLZ" sm row/>-->
|
|
<!-- <tt-input :value="delAddrCity" @input="$emit('update:delAddrCity', $event)" label="Lieferadresse Ort" sm row/>-->
|
|
</template>
|
|
</div>
|
|
`,
|
|
watch: {
|
|
billAddrId: {handler: 'updateBillingMode', immediate: false},
|
|
addressMode: {handler: 'fetchDeliveryAddresses', immediate: false},
|
|
selectedAddr: {handler: 'setSelectedAddrValues', immediate: false},
|
|
newAddrGeoLatLon: {handler: 'fetchGeoAddress', immediate: false},
|
|
},
|
|
methods: {
|
|
async fetchGeoAddress() {
|
|
if (!this.newAddrGeoLatLon) {
|
|
this.$emit('update:delAddrLine', '');
|
|
this.$emit('update:delAddrPLZ', '');
|
|
this.$emit('update:delAddrCity', '');
|
|
return;
|
|
}
|
|
const [lat, lon] = this.newAddrGeoLatLon.split(',');
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/geoReverse?lat=' + lat + '&lon=' + lon);
|
|
|
|
if (response.data.address.road) {
|
|
this.$emit('update:delAddrLine',
|
|
`${response.data.address.road}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
|
|
} else if(response.data.address.village) {
|
|
this.$emit('update:delAddrLine',
|
|
`${response.data.address.village}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
|
|
} else if(response.data.address.hamlet) {
|
|
this.$emit('update:delAddrLine',
|
|
`${response.data.address.hamlet}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
|
|
} else if(response.data.address.residential) {
|
|
this.$emit('update:delAddrLine',
|
|
`${response.data.address.residential}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
|
|
} else if(response.data.address.city) {
|
|
this.$emit('update:delAddrLine',
|
|
`${response.data.address.city}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
|
|
}
|
|
|
|
this.$emit('update:delAddrPLZ', response.data.address.postcode);
|
|
this.$emit('update:delAddrCity', response.data.address.village || response.data.address.city || response.data.address.town);
|
|
},
|
|
async updateBillingMode() {
|
|
await this.fetchDeliveryAddresses();
|
|
|
|
// Here we check if the address is already in the list of addresses, if not we will set the addressMode to billing and fetch the billing address
|
|
if (this.delAddrName && this.delAddrLine && this.delAddrPLZ && this.delAddrCity) {
|
|
const foundAddress = this.addresses.find(address =>
|
|
address.deliveryAddressName === this.delAddrName &&
|
|
address.deliveryAddressLine === this.delAddrLine &&
|
|
address.deliveryAddressPLZ === this.delAddrPLZ &&
|
|
address.deliveryAddressCity === this.delAddrCity);
|
|
if (foundAddress) {
|
|
this.addressMode = 'existing';
|
|
this.selectedAddr = foundAddress.id;
|
|
} else {
|
|
this.addressMode = 'new';
|
|
}
|
|
} else {
|
|
this.addressMode = 'billing';
|
|
await this.fetchBillingAddress();
|
|
}
|
|
},
|
|
async fetchDeliveryAddresses(newVal, oldVal) {
|
|
if ((oldVal === 'billing' || oldVal === 'existing') && newVal === 'new') {
|
|
this.$emit('update:delAddrName', '');
|
|
this.$emit('update:delAddrLine', '');
|
|
this.$emit('update:delAddrPLZ', '');
|
|
this.$emit('update:delAddrCity', '');
|
|
this.$emit('update:delAddrEMail', '');
|
|
return;
|
|
}
|
|
|
|
if (this.addressMode === 'billing' && this.billAddrId) {
|
|
await this.fetchBillingAddress();
|
|
return;
|
|
}
|
|
|
|
if (!this.billAddrId || this.addressMode !== 'existing' || this.fetchedBillAddr === this.billAddrId) return;
|
|
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDeliveryAddresses?billingAddressId=' + this.billAddrId);
|
|
|
|
this.fetchedBillAddr = this.billAddrId;
|
|
this.addresses = response.data.map(address => {
|
|
address.value = address.id;
|
|
address.text = `${address.deliveryAddressName} - ${address.deliveryAddressLine}, ${address.deliveryAddressPLZ} ${address.deliveryAddressCity}`;
|
|
return address;
|
|
});
|
|
},
|
|
setSelectedAddrValues() {
|
|
if (!this.selectedAddr) return;
|
|
|
|
const selectedAddress = this.addresses.find(address => address.id === parseInt(this.selectedAddr));
|
|
if (!selectedAddress) {
|
|
this.window.notify('error', 'Lieferadresse konnte nicht gefunden werden');
|
|
return;
|
|
}
|
|
|
|
this.$emit('update:delAddrName', selectedAddress.deliveryAddressName);
|
|
this.$emit('update:delAddrLine', selectedAddress.deliveryAddressLine);
|
|
this.$emit('update:delAddrPLZ', selectedAddress.deliveryAddressPLZ);
|
|
this.$emit('update:delAddrCity', selectedAddress.deliveryAddressCity);
|
|
},
|
|
async fetchBillingAddress() {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/api?do=getAddress&id=' + this.billAddrId);
|
|
if (response.data.status !== 'OK' || !response.data.result.address) {
|
|
this.window.notify('error', 'Rechnungsadresse konnte nicht gefunden werden');
|
|
return;
|
|
}
|
|
// TODO: here is still a bug that we fetch the billing address twice
|
|
// this.window.notify('success', 'Rechnungsadresse gefunden');
|
|
|
|
this.$emit('update:delAddrName',
|
|
response.data.result.address.company || response.data.result.address.firstname + ' ' + response.data.result.address.lastname);
|
|
this.$emit('update:delAddrLine', response.data.result.address.street);
|
|
this.$emit('update:delAddrPLZ', response.data.result.address.zip);
|
|
this.$emit('update:delAddrCity', response.data.result.address.city);
|
|
this.$emit('update:delAddrEMail', response.data.result.address.email);
|
|
}
|
|
}
|
|
})
|
|
|
|
// now we need a signature pad component which will fire a close or a signed event and takes shipping note as a prop
|
|
// when mounted it will load https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js
|
|
// and display using a tt-modal
|
|
// and when save/submit is clicked we will send it to /WarehouseShippingNote/sign?id=ID POST with the signature as a base64 encoded image string
|
|
|
|
Vue.component('warehouse-shipping-note-signature-pad', {
|
|
props: {
|
|
shippingNoteId: {type: Number, required: true}
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
signaturePad: null,
|
|
shippingNote: null,
|
|
signatureName: '',
|
|
}
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<tt-modal class="signModal" :show="true" :delete="false" :submit="false" @update:show="$emit('close')" :title="'Unterschrift'">
|
|
<div style="max-width: 520px;display: flex; flex-direction: column; align-items: center;">
|
|
<div style="width: 480px">
|
|
<tt-input v-model="signatureName" label="Name" row/>
|
|
</div>
|
|
<div>
|
|
<canvas id="signature-pad" width="500" height="200" style="border: 1px solid black"></canvas>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-primary" @click="submit()">Speichern</button>
|
|
<button class="btn btn-primary" @click="signaturePad.clear()">Leeren</button>
|
|
</div>
|
|
</div>
|
|
</tt-modal>
|
|
`,
|
|
methods: {
|
|
async submit() {
|
|
const data = this.signaturePad.toDataURL();
|
|
const response = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/sign?id=' + this.shippingNoteId,
|
|
{signature: data, signatureName: this.signatureName});
|
|
if (response.data.success) {
|
|
this.window.notify('success', response.data.message || 'Erfolgreich unterschrieben');
|
|
this.$emit('close');
|
|
} else {
|
|
this.window.notify('error',
|
|
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
},
|
|
},
|
|
async mounted() {
|
|
// fetch shipping note by id
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getById?id=' + this.shippingNoteId);
|
|
this.shippingNote = response.data;
|
|
this.signaturePad = new SignaturePad(document.getElementById('signature-pad'));
|
|
}
|
|
}) |