Vue.component('warehouse-shipping-note-modal-text-elements', {
props: {
textElements: Array
},
data() {
return {
window: window,
textElementsData: [],
}
},
//language=Vue
template: `
`,
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: `
`,
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: `
| Mitarbeiter |
Datum |
ST |
KM |
Stundenlohn |
Aktionen |
| Keine Einträge |
| {{ userNames[entry.userId] }} |
{{ window.moment(entry.date).format('DD.MM.YYYY') }} |
{{ entry.hourCount }} |
{{ entry.kilometerCount }} |
{{ entry.hourlyPrice }} |
|
`,
// 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: `
`,
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: `
`,
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: `
| Artikel |
Menge |
Energie Material |
Preis |
Aktionen |
| Keine Einträge |
| {{ position.article ? articleNames[position.article] : position.articlePacket ? articlePacketNames[position.articlePacket] :
position.articleText }}
|
{{ position.amount }} |
{{ position?.isEnergieMaterial ? 'Ja' : 'Nein' }} |
{{ (position.price?.toFixed(2)) }} € |
|
`,
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: `
`,
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: `
Liefer- und Rechnungsadresse
Textelemente
Stunden
Positionen
Bitte füllen Sie die Rechnungs- und Lieferadresse aus
`,
// 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('
') : 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: `
Adresse: {{ delAddrLine }}, {{ delAddrPLZ }} {{ delAddrCity }}
`,
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: `
`,
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('
') : 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'));
}
})