Vue.component('change-status-modal', { props: { orderId: {type: Number, required: true}, type: {type: String, default: 'accept'} }, data() { return { window: window, order: null, newStatus: 'noChanges', note: '', file: null, uploadedFiles: [], deliveryNoteFiles: [], sendEmail: false, sendEmailViewedPDF: false, sendEmailMail: '', submitLoading: false, deliveredPositions: {}, // To track delivery details for each position warehouseLocations: [], selectedLocationId: null }; }, async mounted() { const [orderResponse, locationsResponse] = await Promise.all([ axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.orderId}}), axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getLocations`) ]); if (orderResponse.data.status === 'cancelled') { this.$emit('close'); window.notify('error', 'Bestellung wurde storniert'); } this.order = orderResponse.data; this.warehouseLocations = locationsResponse.data; // Set default location to "K1 Fladnitz 150" if available const defaultLocation = this.warehouseLocations.find(loc => loc.text === 'K1 Fladnitz 150'); this.selectedLocationId = defaultLocation ? defaultLocation.value : (this.warehouseLocations[0]?.value || null); // Initialize deliveredPositions after fetching the order if (this.order && this.order.positions) { this.order.positions.forEach((pos, index) => { this.$set(this.deliveredPositions, index, { amount: pos.amount, // Default delivered amount to the ordered amount reason: '', cancelRest: false, articleName: pos.articleName, // Store for easy access in the submit method orderedAmount: pos.amount }); }); } }, computed: { movementPreviewCount() { if (!this.deliveredPositions) return 0; return Object.values(this.deliveredPositions).filter(p => p.amount > 0).length; }, availableStatuses() { // This computed property remains unchanged switch (this.order.status) { case 'new': return [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'accepted', text: 'Akzeptiert'}, {value: 'ordered', text: 'Bestellt'}, {value: 'cancelled', text: 'Storniert'}, ]; case 'accepted': return [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'ordered', text: 'Bestellt'}, {value: 'cancelled', text: 'Storniert'}, ]; case 'ordered': return [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'sent', text: 'Versendet'}, {value: 'fullyDelivered', text: 'Geliefert'}, {value: 'partiallyDelivered', text: 'Teilweise geliefert'}, {value: 'cancelled', text: 'Storniert'}, ]; case 'sent': return [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'partiallyDelivered', text: 'Teilweise geliefert'}, {value: 'fullyDelivered', text: 'Geliefert'}, {value: 'cancelled', text: 'Storniert'}, ]; case 'partiallyDelivered': return [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'fullyDelivered', text: 'Geliefert'}, {value: 'cancelled', text: 'Storniert'}, ]; case 'fullyDelivered': return [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'cancelled', text: 'Storniert'}, ]; } } }, methods: { async handleFileUpload(event, isDeliveryNote = false) { const files = event.target.files; if (!files.length) return; for (let i = 0; i < files.length; i++) { const file = files[i]; const formData = new FormData(); formData.append('file', file); try { const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/uploadFile`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); if (response.data.success) { const fileEntry = { id: response.data.fileId, name: file.name }; if (isDeliveryNote) { this.deliveryNoteFiles.push(fileEntry); } else { this.uploadedFiles.push(fileEntry); } window.notify('success', `Datei "${file.name}" erfolgreich hochgeladen`); } else { window.notify('error', `Datei "${file.name}" Upload fehlgeschlagen: ${response.data.error || 'Unbekannter Fehler'}`); } } catch (error) { window.notify('error', `Fehler beim Hochladen von "${file.name}"`); } } event.target.value = ''; }, removeFile(index) { this.uploadedFiles.splice(index, 1) }, removeDeliveryNoteFile(index) { this.deliveryNoteFiles.splice(index, 1) }, async submit() { this.submitLoading = true; if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailMail) { window.notify('error', 'Bitte geben Sie eine E-Mail-Adresse ein'); this.submitLoading = false; return; } else if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailViewedPDF) { window.notify('error', 'Bitte öffnen Sie das PDF bevor Sie die E-Mail senden'); this.submitLoading = false; return; } else if (this.newStatus === 'accepted' && this.sendEmail && this.sendEmailMail) { if (this.order.sendShippingNote > 0) { const shippingNoteId = await this.generateShippingNote(); await this.submitEmail(shippingNoteId); } else await this.submitEmail(); } const fileIds = this.uploadedFiles.map(file => file.id); const deliveryNoteFileIds = this.deliveryNoteFiles.map(file => file.id); // Prepare delivery data if the status is related to delivery let deliveryData = null; if (this.newStatus === 'partiallyDelivered' || this.newStatus === 'fullyDelivered') { deliveryData = this.deliveredPositions; } const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createNewLogAction`, { orderId: this.order.id, status: this.newStatus, note: this.note, fileIds: JSON.stringify(fileIds), deliveryData: deliveryData, // Send the new delivery data to the backend locationId: this.selectedLocationId, deliveryNoteFileIds: deliveryNoteFileIds }); if (response.data.success) { this.$emit('close'); window.notify('success', response.data.message ?? 'Status erfolgreich geändert'); } else { window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); } this.submitLoading = false; }, async generateShippingNote() { // This method remains unchanged const positions = this.order.positions.map(position => ({ article: position.article, amount: position.amount, price: position.buyPrice })); const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/create`, { "billingAddressId": this.order.sendShippingNote, "deliveryAddressName": this.order.delAddrName, "deliveryAddressLine": this.order.delAddrLine, "deliveryAddressPLZ": this.order.delAddrPLZ, "deliveryAddressCity": this.order.delAddrCity, "deliveryAddressEMail": this.order.delAddrEMail, "textElements": [], "hoursEntries": [], "positions": positions, "note": "Bestellung #" + this.order.orderNumber, "status": "new" }); return response.data.id; }, async submitEmail(shippingNote = null) { // This method remains unchanged const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/sendEmail?id=${this.order.id}&email=${this.sendEmailMail}${shippingNote ? '&shippingNote=' + shippingNote : ''}`); if (response.data.success) { window.notify('success', response.data.message); } else { window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten'); } } }, template: ` ` }); // The other components in WarehouseOrder.js (warehouse-order-modal, tt-file, etc.) remain unchanged. // I am including them here to provide the full file content. Vue.component('warehouse-order-modal', { props: { id: {type: [String, Number], required: true}, mode: {type: String, default: 'sign'} }, template: `

Bestelldetails


Positionen


Lieferadresse


`, data() { return { window: window, showSendShippingNote: null, lastDistributorFetch: null, positionsConfig: { customOrdering: 'distributorId', fields: { article: { type: 'input-article', label: 'Artikel', apiUrl: '/WarehouseArticle/autoComplete', customFieldReference: 'WarehouseArticle', emitDisplayValue: true, }, distributorId: {type: 'select', label: 'Lieferant', options: [], customFieldReference: 'WarehouseDistributor'}, distributorArticleNumber: {type: 'input', label: 'Lieferant Art-Nr.'}, amount: {type: 'input', label: 'Menge', inputType: 'number'}, buyPrice: {type: 'input', label: 'Einkaufspreis', inputType: 'number'}, verwendung: {type: 'input', label: 'Verwendung'}, }, validateFormOptions: [ {key: 'amount', message: 'Bitte füllen Sie die Menge aus'}, {key: 'distributorId', message: 'Bitte füllen Sie den Lieferanten aus'}, {key: 'article', message: 'Bitte füllen Sie den Artikel aus'}, {key: 'buyPrice', message: 'Bitte füllen Sie den Einkaufspreis aus'} ], }, order: { extReference: '', delAddrName: 'XINON GmbH', delAddrLine: 'Fladnitz im Raabtal 150', delAddrPLZ: '8322', delAddrCity: 'Studenzen', delAddrEMail: 'einkauf@xinon.at', note: '', editor: window.TT_CONFIG['USER_ID'], sendShippingNote: null, positions: [], } } }, async mounted() { if (this.id !== 'create') { const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById?disableParse`, {params: {id: this.id}}); this.order = {...data, positions: JSON.parse(data.positions)}; return; } this.order.editor = parseInt(window.TT_CONFIG['USER_ID']); const orderRequests = JSON.parse(localStorage.getItem('WarehouseOrder_create')); if (!orderRequests) return; for (const orderRequest of orderRequests) { const positions = JSON.parse(orderRequest.positions); const parsedPositions = await Promise.all(positions.map(async p => { const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`, {params: {articleId: p.articleId}})) .data .filter(d => !isNaN(d.purchasePrice)) .sort((a, b) => a.purchasePrice - b.purchasePrice)[0]; return { article: p.articleId, amount: p.amount, buyPrice: distributor.purchasePrice, distributorId: distributor.id, distributorArticleNumber: distributor.externalArticleNumber, verwendung: `${p.hasOwnProperty('purpose') ? p.purpose : ''} [Bestellwunsch: #${orderRequest.id}]`, linkedOrderRequestId: orderRequest.id }; })); this.order.positions = [...this.order.positions, ...parsedPositions]; } localStorage.removeItem('WarehouseOrder_create'); }, methods: { async submit() { if (this.order.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.'); if (this.id === 'create') { const distributorIds = [...new Set(this.order.positions.map(position => position.distributorId))]; for (const distributorId of distributorIds) { const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/create`, { ...this.order, distributorId, positions: this.order.positions.filter(position => position.distributorId === distributorId) } ); if (response.data.success) { this.$emit('close'); window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt'); } else window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); } } else { const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/update`, this.order); if (response.data.success) { this.$emit('close'); window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert'); } else window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); } }, async deleteOrder() { if (!window.confirm('Bestellung wirklich löschen?')) return; const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/delete`, {id: this.id}); if (response.data.success) { this.$emit('close'); window.notify('success', response.data.message || 'Bestellung erfolgreich gelöscht'); } else window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten'); }, async fetchDistributors(article) { const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`; console.log(article); const params = typeof article === 'string' ? {allDistributor: true} : {articleId: article}; if (JSON.stringify(params) === JSON.stringify(this.lastDistributorFetch)) return; this.lastDistributorFetch = params; const response = await axios.get(url, {params}); this.positionsConfig.fields.distributorId.options = response.data.map(distributor => ({ value: distributor.id, text: distributor.name, externalArticleNumber: distributor.externalArticleNumber || null, purchasePrice: distributor.purchasePrice || null, })); }, async fetchDistributorData(distributorId) { if (distributorId && typeof this.$refs.positionsManager.formData.article === 'number') { const distributor = this.positionsConfig.fields.distributorId.options.find(distributor => parseInt(distributor.value) === parseInt(distributorId)); this.$refs.positionsManager.updateField('distributorArticleNumber', distributor.externalArticleNumber); this.$refs.positionsManager.updateField('buyPrice', distributor.purchasePrice); } }, async updateArticlePriceForDistributor(articleId, distributorId, buyPrice) { if (!articleId || !distributorId || !buyPrice) return; const res = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticleDistributor/get`, { filters: { articleId, distributorId } }) const current = res.data.rows[0]; if (current && current.purchasePrice === buyPrice) { window.notify('info', 'Preis ist bereits aktuell'); return; } const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticleDistributor/update`, { ...current, purchasePrice: buyPrice, }); if (response.data.success) { window.notify('success', 'Preis erfolgreich aktualisiert'); } else { window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten'); } } }, watch: { 'order.positions': { handler(newPositions) { if (this.id !== 'create' && new Set(newPositions.map(p => p.distributorId)).size > 1) { window.notify('error', 'Eine bestehende Bestellung kann nur Positionen vom gleichen Lieferanten enthalten.'); this.order.positions = newPositions.filter(p => p.distributorId === this.order.distributorId); } }, deep: true } } , }); Vue.component('tt-file', { props: ['id'], data: () => ({file: null}), async mounted() { const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/File/getById`, {params: {id: this.id}}); this.file = response.data; }, template: `
{{ file.filename }}
` }) Vue.component('warehouse-order-detail', { template: `
`, props: ['id'], data() { return { order: {}, orderLog: null, linkedMovements: [], loading: true, window: window, // Status form state showStatusForm: false, newStatus: 'noChanges', selectedLocationId: null, warehouseLocations: [], deliveredPositions: {}, uploadedFiles: [], deliveryNoteFiles: [], note: '', isSubmitting: false }; }, computed: { grandTotal() { return this.order.positions?.reduce((sum, p) => sum + (p.amount * p.buyPrice), 0) || 0; }, statusLabel() { const labels = { new: 'Neu', accepted: 'Akzeptiert', ordered: 'Bestellt', sent: 'Versendet', partiallyDelivered: 'Teilweise geliefert', fullyDelivered: 'Geliefert', cancelled: 'Storniert' }; return labels[this.order.status] || this.order.status; }, availableStatuses() { const statusMap = { new: [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'accepted', text: 'Akzeptiert'}, {value: 'ordered', text: 'Bestellt'}, {value: 'cancelled', text: 'Storniert'} ], accepted: [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'ordered', text: 'Bestellt'}, {value: 'cancelled', text: 'Storniert'} ], ordered: [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'sent', text: 'Versendet'}, {value: 'fullyDelivered', text: 'Geliefert'}, {value: 'partiallyDelivered', text: 'Teilweise geliefert'}, {value: 'cancelled', text: 'Storniert'} ], sent: [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'partiallyDelivered', text: 'Teilweise geliefert'}, {value: 'fullyDelivered', text: 'Geliefert'}, {value: 'cancelled', text: 'Storniert'} ], partiallyDelivered: [ {value: 'noChanges', text: 'Keine Änderungen'}, {value: 'fullyDelivered', text: 'Geliefert'}, {value: 'cancelled', text: 'Storniert'} ] }; return statusMap[this.order.status] || [{value: 'noChanges', text: 'Keine Änderungen'}]; }, isDeliveryStatus() { return this.newStatus === 'partiallyDelivered' || this.newStatus === 'fullyDelivered'; }, movementPreviewCount() { if (!this.deliveredPositions) return 0; return Object.values(this.deliveredPositions).filter(p => p.amount > 0).length; }, sortedLog() { if (!this.orderLog) return []; return [...this.orderLog].sort((a, b) => b.create - a.create); } }, async mounted() { const [orderResponse, logResponse, movementsResponse, locationsResponse] = await Promise.all([ axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById`, {params: {id: this.id}}), axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}}), axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLinkedMovements`, {params: {orderId: this.id}}), axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLocations`) ]); this.order = orderResponse.data; this.orderLog = logResponse.data; this.linkedMovements = movementsResponse.data || []; this.warehouseLocations = locationsResponse.data || []; // Set default location const defaultLoc = this.warehouseLocations.find(l => l.text === 'K1 Fladnitz 150'); this.selectedLocationId = defaultLoc ? defaultLoc.value : (this.warehouseLocations[0]?.value || null); this.loading = false; }, methods: { formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'), formatCurrency(value) { return new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value); }, getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text || 'Unbekannt', toggleStatusForm() { this.showStatusForm = !this.showStatusForm; if (this.showStatusForm) { this.initDeliveredPositions(); } }, initDeliveredPositions() { this.deliveredPositions = {}; if (this.order && this.order.positions) { this.order.positions.forEach((pos, index) => { this.$set(this.deliveredPositions, index, { amount: pos.amount, reason: '', cancelRest: false, articleName: pos.articleName, orderedAmount: pos.amount }); }); } }, async handleFileUpload(event, isDeliveryNote) { const files = event.target.files; if (!files.length) return; for (const file of files) { const formData = new FormData(); formData.append('file', file); try { const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/uploadFile`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); if (response.data.success) { const entry = { id: response.data.fileId, name: file.name }; if (isDeliveryNote) { this.deliveryNoteFiles.push(entry); } else { this.uploadedFiles.push(entry); } } } catch (e) { window.notify('error', 'Fehler beim Hochladen'); } } event.target.value = ''; }, cancelStatusChange() { this.showStatusForm = false; this.newStatus = 'noChanges'; this.note = ''; this.uploadedFiles = []; this.deliveryNoteFiles = []; }, async submitStatusChange() { this.isSubmitting = true; try { const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/createNewLogAction`, { orderId: this.order.id, status: this.newStatus, note: this.note, fileIds: JSON.stringify(this.uploadedFiles.map(f => f.id)), deliveryData: this.isDeliveryStatus ? this.deliveredPositions : null, locationId: this.selectedLocationId, deliveryNoteFileIds: this.deliveryNoteFiles.map(f => f.id) }); if (response.data.success) { window.notify('success', 'Status erfolgreich geändert'); this.cancelStatusChange(); // Reload data const [orderRes, logRes, movRes] = await Promise.all([ axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById`, {params: {id: this.id}}), axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}}), axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLinkedMovements`, {params: {orderId: this.id}}) ]); this.order = orderRes.data; this.orderLog = logRes.data; this.linkedMovements = movRes.data || []; // Emit event to refresh table this.$emit('status-changed'); } else { window.notify('error', response.data.error || 'Fehler beim Speichern'); } } catch (e) { window.notify('error', 'Netzwerkfehler'); } this.isSubmitting = false; } } }); Vue.component('warehouse-order', { template: ` `, data: () => ({ orderModalId: null }), mounted() { if (JSON.parse(localStorage.getItem('WarehouseOrder_create'))) this.orderModalId = 'create'; }, methods: { async closeModal() { this.orderModalId = null; await new Promise(resolve => setTimeout(resolve, 250)); this.$refs.table.$refs.table.refreshTable(); }, refreshTable() { this.$refs.table.$refs.table.refreshTable(); }, calculateSum: positions => positions.reduce((sum, {amount, buyPrice}) => sum + amount * buyPrice, 0), openPDF: order => window.open(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createPDF?id=${order.id}`) } });