Merge branch 'warehouse-order-improvements' into 'master'

Updated WarehouseOrder and WarehouseOrderRequest

See merge request fronk/thetool!1075
This commit is contained in:
Luca Haid
2025-03-06 09:30:26 +00:00
21 changed files with 1236 additions and 721 deletions
@@ -26,6 +26,83 @@
}
}
.grid-container {
display: grid;
grid-template-columns: 2fr 0.5fr 0.5fr 1fr 2fr 0.5fr;
grid-gap: 10px;
}
.grid-container.header {
margin-top: 24px;
}
.upload-success-alert {
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.alert-header {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 18px;
color: #155724;
}
.alert-header i {
margin-right: 10px;
font-size: 24px;
}
.file-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.file-item {
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 4px;
padding: 10px;
margin-bottom: 8px;
transition: background-color 0.3s ease;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item i {
margin-right: 10px;
color: #6c757d;
}
.file-name {
flex-grow: 1;
font-size: 14px;
}
.remove-btn {
background-color: #dc3545;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.remove-btn:hover {
background-color: #c82333;
}
/* Expanded Row Styling */
.order-summary {
+293 -61
View File
@@ -1,3 +1,174 @@
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},
@@ -6,6 +177,7 @@ Vue.component('warehouse-order-modal', {
template: `
<tt-modal :show="true"
@submit="submit"
@delete="deleteOrder"
:delete="id !== 'create'"
:title="id === 'create' ? 'Bestellung erstellen' : \`Bestellung #\${id} bearbeiten\`"
@update:show="$emit('close')">
@@ -94,13 +266,31 @@ Vue.component('warehouse-order-modal', {
}
},
async mounted() {
if (this.id === 'create') return;
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;
}
console.log(this.id);
const orderRequest = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
if (!orderRequest) return;
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById?disableParse`, {params: {id: this.id}});
response.data.positions = JSON.parse(response.data.positions);
this.order = response.data;
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() {
@@ -131,6 +321,14 @@ Vue.component('warehouse-order-modal', {
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};
@@ -152,96 +350,130 @@ Vue.component('warehouse-order-modal', {
}
},
},
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', {
//language=Vue
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 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>{{order.delAddrName}}</div>
<div>{{order.delAddrEMail}}</div>
<div>{{order.delAddrLine}}</div>
<div>{{order.delAddrPLZ}} {{order.delAddrCity}}</div>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;margin-top: 24px">
<div><strong>Artikel</strong></div>
<div><strong>Menge</strong></div>
<div><strong>Preis</strong></div>
<div><strong>Lieferant</strong></div>
<div><strong>Verwendung</strong></div>
<div><strong>Summe</strong></div>
</div>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;" v-for="position in order.positions">
<div>{{ position.articleName }}</div>
<div>{{ position.amount }}</div>
<div>{{ position.buyPrice }}</div>
<div>{{ position.distributorName }}</div>
<div>{{ position.verwendung }}</div>
<div>{{ position.amount * position.buyPrice }}</div>
</div>
<div v-for="field in ['delAddrName', 'delAddrEMail', 'delAddrLine']">{{ order[field] }}</div>
<div>{{ order.delAddrPLZ }} {{ order.delAddrCity }}</div>
</template>
</tt-card>
`,
props: {
id: {type: [String, Number], required: true}
},
data() {
return {
window: window,
order: {},
loading: true
}
},
props: ['id'],
data: () => ({order: {}, orderLog: null, loading: true}),
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
this.order = response.data;
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="closeOrderModal"/>
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
<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="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseOrder/createPDF?id=' + $event.id)"
@openpdf="openPDF"
@changeStatus="changeStatusModalId = $event.id"
@edit="orderModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<warehouse-order-detail :id="row['id']"/>
<warehouse-order-detail :id="row.id"/>
</template>
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2)}} €</template>
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2) }} €</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
orderModalId: null,
}
data: () => ({
orderModalId: null,
changeStatusModalId: null
}),
mounted() {
if (JSON.parse(localStorage.getItem('WarehouseOrder_create'))) this.orderModalId = 'create';
},
methods: {
closeOrderModal() {
closeModal() {
this.orderModalId = null;
this.changeStatusModalId = null;
this.$refs.table.$refs.table.refreshTable();
},
calculateSum(positions) {
return positions.reduce((sum, position) => sum + position.amount * position.buyPrice, 0);
}
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}`)
}
});
@@ -0,0 +1,49 @@
.WarehouseOrderRequestDetailTable {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 500px;
font-family: Arial, sans-serif;
background-color: #f8f9fa;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 20px;
}
.WarehouseOrderRequestDetailTable > div {
padding: 12px 15px;
text-align: left;
font-weight: bold;
font-size: 14px;
color: #ffffff;
background-color: #4a90e2;
}
.WarehouseOrderRequestDetailTable > div:nth-child(3n+1),
.WarehouseOrderRequestDetailTable > div:nth-child(3n+2),
.WarehouseOrderRequestDetailTable > div:nth-child(3n+3) {
background-color: #2980b9;
}
.WarehouseOrderRequestDetailTable > div:nth-child(n+4) {
background-color: #ffffff;
color: #333333;
font-weight: normal;
border-bottom: 1px solid #e0e0e0;
}
.WarehouseOrderRequestDetailTable > div:nth-child(n+4):nth-child(6n+4),
.WarehouseOrderRequestDetailTable > div:nth-child(n+4):nth-child(6n+5),
.WarehouseOrderRequestDetailTable > div:nth-child(n+4):nth-child(6n+6) {
background-color: #f2f2f2;
}
.WarehouseOrderRequestDetailTable > div:last-child,
.WarehouseOrderRequestDetailTable > div:nth-last-child(2),
.WarehouseOrderRequestDetailTable > div:nth-last-child(3) {
border-bottom: none;
}
.modal-content {
min-height: unset !important;
}
@@ -1,127 +1,247 @@
window.localStorage.setItem('tt-table-WarehouseOrderRequest', JSON.stringify({
filters: {
takeOverBy: null,
canceled: 0
}
}));
window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
...window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"],
{
"key": "cancel",
"title": "Bestellwunsch stornieren",
"class": "fas fa-times text-danger",
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.canceled === 0,
key: "cancelRequest",
title: "Bestellwunsch stornieren",
class: "fas fa-times text-danger",
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.cancelled === 0,
},
{
"key": "uncancel",
"title": "Bestellwunsch wiederherstellen",
"class": "fas fa-check text-success",
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.canceled === 1,
key: "uncancelRequest",
title: "Bestellwunsch wiederherstellen",
class: "fas fa-check text-success",
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.cancelled === 1,
},
{
key: "createOrder",
title: "Bestellung erstellen",
class: "fas fa-plus text-success",
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'
&& row.cancelled === 0 && (!row.linkedOrderIds || row.linkedOrderIds.length === 0)
&& JSON.parse(row.positions).filter(position => position.articleId_text).length === 0,
}
]
Vue.component('add-log-modal', {
props: {
orderRequestId: {type: Number, required: true},
type: {type: String, default: 'accept'}
},
data() {
return {
orderRequest: null,
note: '',
};
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getById`, {params: {id: this.orderRequestId}});
this.orderRequest = response.data;
if (this.orderRequest.cancelled === 1) {
this.$emit('close');
window.notify('error', 'Bestellwunsch wurde storniert');
}
},
methods: {
async submit() {
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/createNewLogAction`, {
orderRequestId: this.orderRequestId,
note: this.note,
});
if (response.data.success) {
this.$emit('close');
window.notify('success', 'Log-Eintrag erstellt');
} 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" :delete="false" @submit="submit" @update:show="$emit('close')" title="Status ändern">
<tt-loader :absolute="false" v-if="!orderRequest"/>
<template v-else>
<tt-textarea label="Bemerkung*" v-model="note" sm/>
</template>
</tt-modal>
`
})
Vue.component('order-request-log', {
props: {orderRequestId: {type: Number, required: true}},
data: () => ({
logs: []
}),
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getLogById`, {params: {orderRequestId: this.orderRequestId}});
this.logs = response.data;
const response2 = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getById`, {params: {id: this.orderRequestId}});
// check if linkedOrderIds is set and if set length > 0 and if so, get the linked orders logs
// and add them to the logs array and sort them by create date
// if response2.data.linkedOrderIds is a string try to parse it
if (typeof response2.data.linkedOrderIds === 'string') {
try {
response2.data.linkedOrderIds = JSON.parse(response2.data.linkedOrderIds);
} catch {}
}
if (response2.data.linkedOrderIds && response2.data.linkedOrderIds.length > 0) {
const linkedOrdersLogs = await Promise.all(
response2.data.linkedOrderIds.map(async (id) => {
const res1 = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id}});
const res2 = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getLogById`, {params: {id}});
return res2.data.map(log => {
log.message = `${res1.data.orderNumber} - ${log.message}`;
return log;
})
})
);
this.logs = this.logs.concat(...linkedOrdersLogs).sort((a, b) => b.create - a.create);
}
},
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
},
//language=Vue
template: `
<div>
<template v-if="logs.length > 0">
<hr>
<h3>Log</h3>
<div v-for="log in logs" :key="log.id" class="alert alert-light">
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
</div>
</template>
</div>
`
})
Vue.component('linked-order-status', {
props: ['linkedOrders'],
data: () => ({
orders: [],
statusTranslations: {
new: 'Neu',
accepted: 'Akzeptiert',
ordered: 'Bestellt',
sent: 'Versendet',
partiallyDelivered: 'Teilweise geliefert',
fullyDelivered: 'Geliefert',
cancelled: 'Storniert',
}
}),
async mounted() {
this.orders = await Promise.all(
JSON.parse(this.linkedOrders).map(id => axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById?id=${id}`).then(response => response.data))
);
},
//language=Vue
template: `
<div>
<span v-for="order in orders"
:key="order.id"
class="badge badge-pill badge-primary mr-1">{{ order.orderNumber }} - {{ statusTranslations[order.status] }}</span>
</div>`
});
Vue.component('warehouse-order-request-detail', {
props: {
positions: {
type: Array,
required: true
}
},
//language=Vue
template: `
<div style="display: flex; justify-content: center; margin-bottom: 10px">
<div class="WarehouseOrderRequestDetailTable">
<div>ARTIKEL</div>
<div>MENGE</div>
<div>ZWECK</div>
<template v-for="position in positions">
<div>
<tt-resolver v-if="position.articleId" reference="WarehouseArticle" :value="position.articleId"/>
<span v-else>{{ position.articleId_text }}</span>
</div>
<div>{{ position.amount }}</div>
<div>{{ position.purpose }}</div>
</template>
</div>
</div>
`
});
Vue.component('warehouse-order-request', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"
@cancel="cancelOrderRequest($event, '1')"
@uncancel="cancelOrderRequest($event, '0')"
<tt-table-crud @openHistory="openHistory"
@cancelRequest="cancelRequest"
@uncancelRequest="uncancelRequest"
@createLog="createLog"
@createOrder="createOrder"
ref="crud">
<!-- <slot name="table-top-buttons"></slot> add checkbox "Ausgeblendete Bestellungen anzeigen-->
<template v-slot:table-top-buttons>
<div class="d-flex">
<tt-button
class="mr-2"
@click="showHiddenRequests = !showHiddenRequests"
:text="showHiddenRequests ? 'Erledigte Bestellungen ausblenden' : 'Erledigte Bestellungen anzeigen'"
:additional-class="showHiddenRequests ? 'btn-danger' : 'btn-primary'"/>
<tt-button @click="showCanceledRequests = !showCanceledRequests"
:text="showCanceledRequests ? 'Stornierte Bestellungen ausblenden' : 'Stornierte Bestellungen anzeigen'"
:additional-class="showCanceledRequests ? 'btn-danger' : 'btn-primary'"/>
</div>
<template #linkedorderids="{row}">
<linked-order-status :linkedOrders="row.linkedOrderIds" v-if="row.linkedOrderIds"/>
</template>
<template v-slot:create="{ row }">
{{ row.create ? window.moment(row.create * 1000).format('DD.MM.YYYY') : '' }}
<template #note="{row}">
<span v-if="row.note?.length > 45" :title="row.note">{{ row.note.substring(0, 45) }}...</span>
<span v-else>{{ row.note }}</span>
</template>
<template v-slot:order="{ row }">
{{ row.order ? window.moment(row.order * 1000).format('DD.MM.YYYY') : '' }}
<template #expandedRow="{row}">
<warehouse-order-request-detail :positions="JSON.parse(row['positions'])"/>
<order-request-log :orderRequestId="row.id"/>
</template>
<template v-slot:takeover="{ row }">
{{ row.takeOver ? window.moment(row.takeOver * 1000).format('DD.MM.YYYY') : '' }}
</template>
<template v-slot:note="{ row }">
<span v-if="row.note && row.note.length > 45" :title="row.note">{{ row.note.substring(0, 45) }}...</span>
<span v-else>{{ row.note }}</span>
</template>
</tt-table-crud>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
<add-log-modal v-if="addLogModalId" :orderRequestId="addLogModalId" @close="addLogModal = false; addLogModalId = null"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null, showHiddenRequests: false, showCanceledRequests: false
}
},
async mounted() {
if (this.window.TT_CONFIG.WAREHOUSE_ADMIN !== '1') return
this.$refs.crud.$watch('crudModal', (value) => {
return
if (value) {
// if id is not 'create' then check if order is set and if not set it to current date
// if order is set then check if takeover is set and if not set it to current date
if (!this.$refs.crud.crudModalData.id) return
if (!this.$refs.crud.crudModalData.order) {
this.$refs.crud.crudModalData.order = window.moment().unix()
this.$refs.crud.crudModalData.orderBy = window.TT_CONFIG.user_id
this.$refs.crud.$refs["order-modal-input"][0].setStartDate(window.moment().format('MM/DD/YYYY'))
return
}
if (!this.$refs.crud.crudModalData.takeover) {
this.$refs.crud.crudModalData.takeover = window.moment().unix
this.$refs.crud.crudModalData.takeOverBy = window.TT_CONFIG.user_id
this.$refs.crud.$refs["takeover-modal-input"][0].setStartDate(window.moment().format('MM/DD/YYYY'))
}
}
})
},
methods: {
// portected function cancelAction() {
// $id = $this->request->id;
// $cancel = $this->request->cancel;
async cancelOrderRequest(row, cancel) {
if (!window.confirm('Bestellwunsch wirklich stornieren?')) return
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseOrderRequest/cancel?id=' + row.id + '&cancel=' + cancel);
if (response.data.success) {
this.window.notify('success', response.data.message || 'Erfolgreich aktualisiert')
this.$refs.crud.$refs.table.refreshTable()
return
}
this.window.notify('error', response.data.message || 'Fehler beim aktualisieren')
}
},
watch: {
async showHiddenRequests(value) {
this.showCanceledRequests = false
this.$refs.crud.$refs.table.$set(this.$refs.crud.$refs.table.filters, 'canceled', '')
this.$refs.crud.$refs.table.$set(this.$refs.crud.$refs.table.filters, 'takeOverBy', value ? '' : null)
`,
data: () => ({
window,
historyModal: false,
historyModalId: null,
addLogModal: false,
addLogModalId: null,
showHiddenRequests: false,
showCanceledRequests: false,
orderRequestModalId: null
}),
methods: {
openHistory(e) {
this.historyModal = true;
this.historyModalId = e.id;
},
async showCanceledRequests(value) {
this.showHiddenRequests = false
this.$refs.crud.$refs.table.$set(this.$refs.crud.$refs.table.filters, 'canceled', value ? '1' : '')
this.$refs.crud.$refs.table.$set(this.$refs.crud.$refs.table.filters, 'takeOverBy', '')
async cancelRequest(row, cancel) {
if (!confirm('Bestellwunsch wirklich stornieren?')) return;
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrderRequest/cancel?id=${row.id}&cancel=${cancel}`);
window.notify(res.data.success ? 'success' : 'error',
res.data.message || (res.data.success ? 'Erfolgreich aktualisiert' : 'Fehler beim aktualisieren'));
if (res.data.success) this.$refs.crud.$refs.table.refreshTable();
},
async createLog(row) {
this.addLogModal = true;
this.addLogModalId = row.id;
},
uncancelRequest(row) {
this.cancelRequest(row, '0');
},
async createOrder(row) {
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrderRequest/getById?id=${row.id}`);
if (res.data?.positions && typeof res.data.positions === 'string') {
localStorage.setItem('WarehouseOrder_create', JSON.stringify(res.data));
window.location.href = `${window.TT_CONFIG.BASE_PATH}/WarehouseOrder`;
} else window.notify('error', res.data.message || 'Fehler beim erstellen der Bestellung');
}
}
})
});
@@ -1,161 +1,11 @@
Vue.component('warehouse-project-modal', {
props: {
id: { type: [String, Number], required: true },
mode: { type: String, default: 'edit' }
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Projekt erstellen' : \`Projekt #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%">
<h4 class="text-center">Projektübersicht</h4>
<tt-input label="Projektnummer" v-model="project.projectNumber" sm row disabled />
<tt-textarea label="Um was handelt es sich?" v-model="project.description" sm row/>
<hr>
<h4 class="text-center">Zeitraum</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr;">
<tt-date-picker label="Startdatum" v-model="project.startDate" sm/>
<tt-date-picker label="Enddatum" v-model="project.endDate" sm/>
</div>
<hr>
<h4 class="text-center">Beteiligte Personen</h4>
<tt-select label="Personen (XINON MT)"
:options="participantsOptions"
v-model="project.participants"
sm row />
<tt-textarea label="Freitext für weitere Personen" v-model="project.additionalParticipants" sm row/>
<hr>
<h4 class="text-center">Projektübersicht</h4>
<tt-input label="Gesamtsumme des Projekts (€)" v-model.number="project.totalSum" sm row type="number"/>
<tt-positions-manager
ref="positionsManager"
v-model="project.positions"
:config="positionsConfig"
@updateField-article="fetchArticleData"
/>
<hr>
<h4 class="text-center">Lagerort</h4>
<tt-input label="Lagerort für dieses Projekt" v-model="project.storageLocation" sm row/>
<hr>
<tt-textarea label="Notizen" v-model="project.notes" sm row/>
</div>
</tt-modal>
`,
data() {
return {
window: window,
participantsOptions: [
{ value: 1, text: 'Person A' },
{ value: 2, text: 'Person B' },
{ value: 3, text: 'Person C' }
// Add more participants as needed
],
positionsConfig: {
fields: {
article: {
type: 'autocomplete',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
},
hoursRequired: { type: 'input', label: 'Benötigte Stunden', inputType: 'number' },
amountRequired: { type: 'input', label: 'Benötigte Menge', inputType: 'number' },
description: { type: 'textarea', label: 'Beschreibung' }
},
validateForm(formData) {
const requiredFields = ['article', 'hoursRequired', 'amountRequired'];
for (const field of requiredFields) {
if (!formData[field]) {
window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`);
return false;
}
}
return true;
}
},
project: {
projectNumber: '',
description: '',
startDate: null,
endDate: null,
participants: [],
additionalParticipants: '',
totalSum: 0,
positions: [],
storageLocation: '',
notes: ''
}
};
},
async mounted() {
if (this.id !== 'create') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getById`, { params: { id: this.id } });
this.project = response.data;
} else {
this.project.projectNumber = await this.generateProjectNumber();
}
},
methods: {
async submit() {
if (!this.project.description) return window.notify('error', 'Bitte geben Sie eine Beschreibung ein.');
const url = this.id === 'create'
? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/create`
: `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/update`;
const response = await axios.post(url, this.project);
if (response.data.success) {
window.notify('success', response.data.message ?? 'Projekt erfolgreich gespeichert');
this.$emit('close');
} else {
window.notify('error', response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
},
async fetchArticleData(article) {
if (typeof article === 'number') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, { params: { id: article } });
this.$refs.positionsManager.updateField('description', response.data.description);
}
},
async generateProjectNumber() {
const currentCount = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/count`);
return `PRJ-${new Date().getFullYear()}-${String(currentCount.data + 1).padStart(4, '0')}`;
}
}
});
Vue.component('warehouse-project', {
template: `
<tt-card>
<warehouse-project-modal v-if="projectModalId" :id="projectModalId" @close="projectModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="projectModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<tt-table-crud emit-edit @edit="projectModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<div>
<h5>Notizen</h5>
<p>{{ row.notes }}</p>
<h5>Verlauf</h5>
<ul>
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
</ul>
</div>
</template>
<tt-table-crud ref="table">
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
projectModalId: null,
}
return {window: window}
},
});