480 lines
22 KiB
JavaScript
480 lines
22 KiB
JavaScript
Vue.component('change-status-modal', {
|
|
props: {
|
|
orderId: {type: Number, required: true},
|
|
type: {type: String, default: 'accept'}
|
|
},
|
|
data() {
|
|
return {
|
|
order: null,
|
|
newStatus: 'noChanges',
|
|
note: '',
|
|
file: null,
|
|
uploadedFiles: []
|
|
};
|
|
},
|
|
async mounted() {
|
|
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.orderId}});
|
|
// if order.status is canceled emit close event and window.notify('error', 'Bestellung wurde storniert')
|
|
if (response.data.status === 'cancelled') {
|
|
this.$emit('close');
|
|
window.notify('error', 'Bestellung wurde storniert');
|
|
}
|
|
this.order = response.data;
|
|
|
|
|
|
},
|
|
computed: {
|
|
availableStatuses() {
|
|
switch (this.order.status) {
|
|
case 'new':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'accepted', text: 'Akzeptiert'},
|
|
{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: '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) {
|
|
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) {
|
|
this.uploadedFiles.push({
|
|
id: response.data.fileId,
|
|
name: file.name
|
|
});
|
|
window.notify('success', `File "${file.name}" uploaded successfully`);
|
|
} else {
|
|
window.notify('error', `File "${file.name}" upload failed: ${response.data.error || 'Unknown error'}`);
|
|
}
|
|
} catch (error) {
|
|
window.notify('error', `Error uploading file "${file.name}"`);
|
|
}
|
|
}
|
|
|
|
// Clear the file input
|
|
event.target.value = '';
|
|
},
|
|
removeFile: index => this.uploadedFiles.splice(index, 1),
|
|
async submit() {
|
|
const fileIds = this.uploadedFiles.map(file => file.id);
|
|
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)
|
|
});
|
|
|
|
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('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<tt-modal :show="true" @submit="submit" @update:show="$emit('close')" title="Status ändern">
|
|
<tt-loader :absolute="false" v-if="!order"/>
|
|
<template v-else>
|
|
<tt-select label="Neuer Status" v-model="newStatus" :options="availableStatuses" sm row/>
|
|
|
|
<div class="form-group" style="margin: 10px 0">
|
|
<label>Dateiupload (Mehrere)</label>
|
|
<input type="file" class="form-control" @change="handleFileUpload" multiple/>
|
|
</div>
|
|
|
|
<div v-if="uploadedFiles.length" class="upload-success-alert">
|
|
<div class="alert-header">
|
|
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
|
<span v-if="uploadedFiles.length === 1">Datei erfolgreich hochgeladen</span>
|
|
<span v-else>Dateien erfolgreich hochgeladen</span>
|
|
</div>
|
|
<ul class="file-list">
|
|
<li v-for="(file, index) in uploadedFiles" :key="file.id" class="file-item">
|
|
<i class="fa fa-file" aria-hidden="true"></i>
|
|
<span class="file-name">{{ file.name }}</span>
|
|
<button type="button" class="remove-btn" @click="removeFile(index)">
|
|
<i class="fa fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
|
|
<tt-textarea label="Bemerkung*" v-model="note" sm/>
|
|
|
|
<div v-if="newStatus === 'partiallyDelivered' || newStatus === 'fullyDelivered'">
|
|
<h4>Positionen</h4>
|
|
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr;margin-top: 24px">
|
|
<div><strong>Artikel</strong></div>
|
|
<div><strong>Menge</strong></div>
|
|
<div><strong>Geliefert?</strong></div>
|
|
<template v-for="position in order.positions">
|
|
<div>{{ position.articleName }}</div>
|
|
<div>{{ position.amount }}</div>
|
|
<div><input type="checkbox" v-model="position.delivered"/></div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
</template>
|
|
</tt-modal>
|
|
|
|
`
|
|
});
|
|
|
|
Vue.component('warehouse-order-modal', {
|
|
props: {
|
|
id: {type: [String, Number], required: true},
|
|
mode: {type: String, default: 'sign'}
|
|
},
|
|
template: `
|
|
<tt-modal :show="true"
|
|
@submit="submit"
|
|
@delete="deleteOrder"
|
|
:delete="id !== 'create'"
|
|
:title="id === 'create' ? 'Bestellung erstellen' : \`Bestellung #\${id} bearbeiten\`"
|
|
@update:show="$emit('close')">
|
|
<div style="width: 99%">
|
|
<h4 class="text-center">Bestelldetails</h4>
|
|
<tt-select label="Bearbeiter (XINON)"
|
|
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
|
|
sm
|
|
row
|
|
v-model="order.editor"/>
|
|
<tt-input label="Externe Referenz" v-model="order.extReference" sm row/>
|
|
|
|
<hr>
|
|
<h4 class="text-center">Positionen</h4>
|
|
<tt-positions-manager
|
|
ref="positionsManager"
|
|
v-model="order.positions"
|
|
:config="positionsConfig"
|
|
@updateField-article="fetchDistributors"
|
|
@updateField-distributorId="fetchDistributorData"
|
|
/>
|
|
|
|
<hr>
|
|
<h4 class="text-center">Lieferadresse</h4>
|
|
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 2fr 1fr 1fr 2fr;">
|
|
<tt-input label="Name" v-model="order.delAddrName" sm/>
|
|
<tt-input label="Straße" v-model="order.delAddrLine" sm/>
|
|
<tt-input label="PLZ" v-model="order.delAddrPLZ" sm/>
|
|
<tt-input label="Ort" v-model="order.delAddrCity" sm/>
|
|
<tt-input label="E-Mail" v-model="order.delAddrEMail" sm/>
|
|
</div>
|
|
|
|
<hr>
|
|
<tt-textarea label="Notiz" v-model="order.note" sm row/>
|
|
</div>
|
|
</tt-modal>
|
|
`,
|
|
|
|
data() {
|
|
return {
|
|
window: window,
|
|
positionsConfig: {
|
|
customOrdering: 'distributorId',
|
|
fields: {
|
|
article: {
|
|
type: 'autocomplete',
|
|
label: 'Artikel',
|
|
apiUrl: '/WarehouseArticle/autoComplete',
|
|
customFieldReference: 'WarehouseArticle',
|
|
},
|
|
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'},
|
|
},
|
|
validateForm: (formData) => {
|
|
const fields = [
|
|
{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'}
|
|
];
|
|
|
|
for (const field of fields) {
|
|
if (!formData[field.key]) {
|
|
window.notify('error', field.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
},
|
|
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'],
|
|
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;
|
|
}
|
|
|
|
const orderRequest = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
|
|
if (!orderRequest) return;
|
|
|
|
const positions = JSON.parse(orderRequest.positions);
|
|
this.order.positions = await Promise.all(positions.map(async p => {
|
|
const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`,
|
|
{params: {articleId: p.articleId}})).data[0];
|
|
return {
|
|
article: p.articleId,
|
|
amount: p.amount,
|
|
buyPrice: distributor.purchasePrice,
|
|
distributorId: distributor.id,
|
|
distributorArticleNumber: distributor.externalArticleNumber,
|
|
verwendung: `${p.purpose} [Bestellwunsch: #${orderRequest.id}]`,
|
|
linkedOrderRequestId: orderRequest.id
|
|
};
|
|
}));
|
|
|
|
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('<br>') : 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('<br>') : 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`;
|
|
const params = typeof article === 'string' ? {allDistributor: true} : {articleId: article};
|
|
|
|
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);
|
|
}
|
|
},
|
|
},
|
|
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: `
|
|
<div>
|
|
<a :href="'/File/download?id=' + id" target="_blank" v-if="file">{{ file.filename }}</a>
|
|
<template v-else>
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
|
</template>
|
|
</div>
|
|
`
|
|
})
|
|
|
|
|
|
Vue.component('warehouse-order-detail', {
|
|
template: `
|
|
<tt-card>
|
|
<template v-slot:header><h4>Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}</h4></template>
|
|
<template v-if="loading">
|
|
<div class="d-flex justify-content-center align-items-center">
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<h3>Positionen</h3>
|
|
<div class="grid-container header">
|
|
<div v-for="header in ['Artikel', 'Menge', 'Preis', 'Lieferant', 'Verwendung', 'Summe']"><strong>{{ header }}</strong></div>
|
|
</div>
|
|
<div class="grid-container" v-for="p in order.positions">
|
|
<div>{{ p.articleName }}</div>
|
|
<div>{{ p.amount }}</div>
|
|
<div>{{ p.buyPrice }}</div>
|
|
<div>{{ p.distributorName }}</div>
|
|
<div>{{ p.verwendung }}</div>
|
|
<div>{{ p.amount * p.buyPrice }}</div>
|
|
</div>
|
|
<template v-if="orderLog?.length > 0">
|
|
<hr>
|
|
<h3>Log</h3>
|
|
<div v-for="log in orderLog">
|
|
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
|
|
<!-- if log.fileIds exists and it is a array of ids use <tt-file :id=> to show the file-->
|
|
|
|
<template v-if="log.fileIds">
|
|
<div v-for="file in JSON.parse(log.fileIds)">
|
|
<tt-file :id="file"/>
|
|
</div>
|
|
</template>
|
|
|
|
</div>
|
|
</template>
|
|
<hr>
|
|
<h3>Lieferadresse</h3>
|
|
<div v-for="field in ['delAddrName', 'delAddrEMail', 'delAddrLine']">{{ order[field] }}</div>
|
|
<div>{{ order.delAddrPLZ }} {{ order.delAddrCity }}</div>
|
|
</template>
|
|
</tt-card>
|
|
`,
|
|
props: ['id'],
|
|
data: () => ({order: {}, orderLog: null, loading: true}),
|
|
async mounted() {
|
|
const [orderResponse, logResponse] = 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}})
|
|
]);
|
|
this.order = orderResponse.data;
|
|
this.orderLog = logResponse.data;
|
|
this.loading = false;
|
|
},
|
|
methods: {
|
|
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
|
|
getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text
|
|
}
|
|
});
|
|
|
|
Vue.component('warehouse-order', {
|
|
template: `
|
|
<tt-card>
|
|
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="closeModal"/>
|
|
<change-status-modal v-if="changeStatusModalId" :orderId="changeStatusModalId" @close="closeModal"/>
|
|
<tt-button text="Bestellung erstellen" @click="orderModalId = 'create'" additional-class="btn-primary"/>
|
|
<tt-table-crud emit-edit
|
|
@openpdf="openPDF"
|
|
@changeStatus="changeStatusModalId = $event.id"
|
|
@edit="orderModalId = $event.id" ref="table">
|
|
<template v-slot:expandedRow="{ row }">
|
|
<warehouse-order-detail :id="row.id"/>
|
|
</template>
|
|
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2) }} €</template>
|
|
</tt-table-crud>
|
|
</tt-card>
|
|
`,
|
|
data: () => ({
|
|
orderModalId: null,
|
|
changeStatusModalId: null
|
|
}),
|
|
mounted() {
|
|
if (JSON.parse(localStorage.getItem('WarehouseOrder_create'))) this.orderModalId = 'create';
|
|
},
|
|
methods: {
|
|
closeModal() {
|
|
this.orderModalId = null;
|
|
this.changeStatusModalId = null;
|
|
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}`)
|
|
}
|
|
});
|