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



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: `
`, 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')); } })