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: `
Dateiupload (Mehrere)
Lagerstandort
Positionen Lieferung erfassen
Artikel
Bestellt
Geliefert
Grund für Abweichung
Rest stornieren
{{ position.articleName }}
{{ position.amount }}
Es werden {{ movementPreviewCount }} Lagerbewegung(en) erstellt.
Lieferschein Foto
Lieferschein hochladen
`
});
// 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: `
`
})
Vue.component('warehouse-order-detail', {
template: `
Positionen
Artikel
Menge
Einzelpreis
Summe
{{ p.articleName }}
{{ p.amount }} Stk
{{ formatCurrency(p.buyPrice) }}
{{ formatCurrency(p.amount * p.buyPrice) }}
Gesamtsumme
{{ formatCurrency(grandTotal) }}
Lagerbewegungen
Nummer
Artikel
Menge
Lagerort
Datum
{{ m.movementNumber }}
{{ m.articleName }}
+{{ m.quantity }}
{{ m.locationName }}
{{ formatDate(m.create) }}
Aktivität
`,
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: `
{{ calculateSum(JSON.parse(row["positions"])).toFixed(2) }} €
`,
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}`)
}
});