Feature/warehouse

This commit is contained in:
Luca Haid
2024-07-16 06:55:46 +00:00
parent 8b4589523e
commit e75f13f8d8
64 changed files with 3207 additions and 265 deletions
@@ -0,0 +1,13 @@
Vue.component('default-crud-view', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null,
}
},
})
@@ -0,0 +1,287 @@
Vue.component('warehouse-distributor-modal', {
//language=Vue
template: `
<tt-modal :show.sync="showModal" title="Lieferanten" :delete="false" :save="false"
@close="$emit('update:show', false)">
<div v-for="(row, index) in rows" :key="index" style="display:grid;grid-template-columns: auto auto">
<div style="display:grid; grid-template-columns: auto auto auto">
<tt-autocomplete v-model="row.distributorId"
:api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseDistributor/autocomplete'"></tt-autocomplete>
<tt-input v-model="row.purchasePrice" placeholder="Price" type="number" sm></tt-input>
<tt-input v-model="row.externalArticleNumber" placeholder="External Article Number" sm></tt-input>
</div>
<div class="btn-group mb-4">
<a class="btn btn-sm btn-primary" href="#" @click="saveRow(index)" title="Save Row">
<i class="fas fa-check"></i>
</a>
<a class="btn btn-sm btn-danger" href="#" @click="deleteRow(index)" title="Delete Row">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Lieferant hinzufügen</a>
</tt-modal>
`, props: {
show: {type: Boolean, default: false}, id: {type: Number, default: null},
}, data() {
return {
window: window, showModal: false, rows: []
};
}, async mounted() {
}, methods: {
async fetchArticleDistributor() {
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/get', {filters: {articleId: this.id}});
this.rows = response.data.rows
}, addRow() {
this.rows.push({distributor: null, price: null, externalArticleNumber: null});
}, async saveRow(index) {
// post to /WarehouseArticleDistributor/save with rows data and articleId
const row = this.rows[index];
const data = {
articleId: this.id, distributorId: row.distributorId, purchasePrice: row.purchasePrice, externalArticleNumber: row.externalArticleNumber
}
if (row.id) {
data.id = row.id;
}
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${row.id ? 'update' : 'create'}`, data);
this.$emit('doUpdate');
this.window.notify(response.data.success ? 'success' : 'error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
await this.fetchArticleDistributor();
}, async deleteRow(index) {
const row = this.rows[index];
if (!row.id) {
this.window.notify('error', 'Eintrag hat keine ID');
}
const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/delete', {id: row.id});
this.$emit('doUpdate');
this.window.notify(response.data.success ? 'success' : 'error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
await this.fetchArticleDistributor();
}
}, watch: {
show(newVal) {
this.showModal = newVal;
if (!newVal) {
this.rows = [];
return;
}
this.rows = [];
this.fetchArticleDistributor().then();
}
}
});
Vue.component('warehouse-threshold-modal', {
//language=Vue
template: `
<tt-modal :show.sync="showModal" title="Schwellenwerte" :delete="false" :save="false" @close="$emit('update:show', false)">
<div v-for="(row, index) in rows" :key="index" style="display:grid;grid-template-columns: auto auto">
<div style="display:grid; grid-template-columns: auto auto auto">
<tt-autocomplete v-model="row.locationId"
:api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocation/autocomplete'"></tt-autocomplete>
<tt-input v-model="row.warningAmount" placeholder="Warnmenge" type="number" sm></tt-input>
<tt-input v-model="row.criticalAmount" placeholder="Kritische Menge" type="number" sm></tt-input>
</div>
<div class="btn-group mb-4">
<a class="btn btn-sm btn-primary" href="#" @click="saveRow(index)" title="Save Row">
<i class="fas fa-check"></i>
</a>
<a class="btn btn-sm btn-danger" href="#" @click="deleteRow(index)" title="Delete Row">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Schwellenwert hinzufügen</a>
</tt-modal>
`, props: {
show: {type: Boolean, default: false}, id: {type: Number, default: null},
}, data() {
return {
window: window, showModal: false, rows: []
};
}, async mounted() {
}, methods: {
async fetchLocationThreshold() {
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/get', {filters: {articleId: this.id}});
this.rows = response.data.rows
}, addRow() {
this.rows.push({distributor: null, price: null, externalArticleNumber: null});
}, async saveRow(index) {
// post to /WarehouseArticleDistributor/save with rows data and articleId
const row = this.rows[index];
const data = {
articleId: this.id, locationId: row.locationId, warningAmount: row.warningAmount, criticalAmount: row.criticalAmount
}
if (row.id) {
data.id = row.id;
}
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseLocationThresholdOverride/${row.id ? 'update' : 'create'}`, data);
this.$emit('doUpdate');
this.window.notify(response.data.success ? 'success' : 'error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
await this.fetchLocationThreshold()
}, async deleteRow(index) {
const row = this.rows[index];
if (!row.id) {
this.window.notify('error', 'Eintrag hat keine ID');
}
const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/delete', {id: row.id});
this.$emit('doUpdate');
this.window.notify(response.data.success ? 'success' : 'error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
await this.fetchLocationThreshold();
}
}, watch: {
show(newVal) {
this.showModal = newVal;
if (!newVal) {
this.rows = [];
return;
}
this.rows = [];
this.fetchLocationThreshold().then();
}
}
});
Vue.component('warehouse-article-price-modal', {
//language=Vue
template: `
<tt-modal :show.sync="showModal" title="Artikel-Preise" :delete="false" :save="false" @close="$emit('update:show', false)">
<div v-for="(row, index) in rows" :key="index" style="display:grid;grid-template-columns: auto auto">
<div style="display:grid; grid-template-columns: auto auto auto">
<tt-autocomplete v-model="row.articlePriceTypeId"
:api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePriceType/autocomplete'"></tt-autocomplete>
<tt-input v-model="row.priceMultiplier" placeholder="Preis Multiplikator" type="number" sm></tt-input>
<tt-input v-model="row.priceOverride" placeholder="Preis" type="number" sm></tt-input>
</div>
<div class="btn-group mb-4">
<a class="btn btn-sm btn-primary" href="#" @click="saveRow(index)" title="Save Row">
<i class="fas fa-check"></i>
</a>
<a class="btn btn-sm btn-danger" href="#" @click="deleteRow(index)" title="Delete Row">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Preis hinzufügen</a>
</tt-modal>
`, props: {
show: {type: Boolean, default: false}, id: {type: Number, default: null},
}, data() {
return {
window: window, showModal: false, rows: []
};
}, async mounted() {
}, methods: {
async fetchLocationThreshold() {
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}});
this.rows = response.data.rows
}, addRow() {
this.rows.push({articlePriceTypeId: null, priceMultiplier: null, priceOverride: null});
}, async saveRow(index) {
// post to /WarehouseArticleDistributor/save with rows data and articleId
const row = this.rows[index];
const data = {
articleId: this.id, articlePriceTypeId: row.articlePriceTypeId, priceMultiplier: row.priceMultiplier, priceOverride: row.priceOverride
}
if (row.id) {
data.id = row.id;
}
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${row.id ? 'update' : 'create'}`, data);
this.$emit('doUpdate');
this.window.notify(response.data.success ? 'success' : 'error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
await this.fetchLocationThreshold()
}, async deleteRow(index) {
const row = this.rows[index];
if (!row.id) {
this.window.notify('error', 'Eintrag hat keine ID');
}
const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/delete', {id: row.id});
this.$emit('doUpdate');
this.window.notify(response.data.success ? 'success' : 'error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
await this.fetchLocationThreshold();
}
}, watch: {
show(newVal) {
this.showModal = newVal;
if (!newVal) {
this.rows = [];
return;
}
this.rows = [];
this.fetchLocationThreshold().then();
}
}
})
Vue.component('warehouse-article', {
//language=Vue
template: `
<tt-card>
<tt-table-crud ref="table"
@openHistory="historyModal = true; historyModalId = $event.id"
@editDistributorEntries="distributorModal = true; distributorModalId = $event.id"
@editPricesEntries="priceModal = true; priceModalId = $event.id"
@editThresholdEntries="thresholdModal = true; thresholdModalId = $event.id">
<template v-slot:cheapestPurchasePrice="{ row }">
<span>{{(row.cheapestPurchasePrice * row.sellPriceMultiplier).toFixed(2)}} €</span>
</template>
</tt-table-crud>
<warehouse-distributor-modal :show.sync="distributorModal" :id="distributorModalId" @doUpdate="refreshTable"/>
<warehouse-threshold-modal :show.sync="thresholdModal" :id="thresholdModalId" @doUpdate="refreshTable"/>
<warehouse-article-price-modal :show.sync="priceModal" :id="priceModalId" @doUpdate="refreshTable"/>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window,
historyModal: false,
historyModalId: null,
distributorModal: false,
distributorModalId: null,
thresholdModal: false,
thresholdModalId: null,
priceModal: false,
priceModalId: null
}
}, methods: {
refreshTable() {
this.$refs.table.$refs.table.refreshTable();
}
}
})
@@ -0,0 +1,13 @@
Vue.component('warehouse-distributor', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null,
}
},
})
@@ -0,0 +1,130 @@
Vue.component('tt-expandable-shopping-cart', {
props: {
cartItems: Array,
},
data() {
return {
isExpanded: false,
};
},
methods: {
},
template: `
<div class="tt-expandable-shopping-cart" :class="{ expanded: isExpanded }">
<button class="toggle-button" @click="isExpanded = !isExpanded">
<i class="fas fa-shopping-cart text-primary"></i>
<span v-if="cartItems.length > 0 && isExpanded" class="btn btn-primary" @click.prevent="$emit('submitOrder')">Bestellen</span>
<span class="cart-count" v-if="cartItems.length > 0">{{ cartItems.length }}</span>
<!-- add arrow down icon when cart is expanded additionally with v-if -->
<i v-if="isExpanded" class="fas fa-arrow-down text-danger" style="font-size:21px;grid-column: 4;"></i>
</button>
<div class="cart-content" v-if="isExpanded">
<div v-if="cartItems.length > 0">
<h3>Einkaufswagen</h3>
<ul class="list-group">
<template v-for="item in cartItems">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ item.title }}
<span class="badge badge-primary badge-pill">{{ item.amount }}</span>
</li>
</template>
</ul>
</div>
<p v-else>Der Einkaufswagen ist leer.</p>
</div>
</div>
`
});
Vue.component('warehouse-e-shop', {
//language=Vue
template: `
<tt-card>
<tt-expandable-shopping-cart :cart-items="shoppingCart" @submitOrder="openOrderDialog"/>
<tt-modal :show.sync="createOrderDialog"
:delete="false"
title="Bestellung abschicken"
@submit="submitOrder">
<tt-select v-model="createOrderDialogData.deliveryMode" label="Adresse" :options="[
{text: 'Einzelne Adresse', value: 'singleAddress'},
{text: 'Mehrere Adressen', value: 'multipleAddresses'},
]" sm row/>
<tt-input v-model="createOrderDialogData.deliveryAddressName" label="Name" sm row/>
<tt-input v-model="createOrderDialogData.deliveryAddressLine" label="Straße" sm row/>
<tt-input v-model="createOrderDialogData.deliveryAddressPLZ" label="PLZ" sm row/>
<tt-input v-model="createOrderDialogData.deliveryAddressCity" label="Stadt" sm row/>
</tt-modal>
<tt-table-crud>
<template v-slot:amount="{ row }">
<!-- this has no padding - add a full width full height tt-input with -->
<tt-input type="number" style="width: 100%; height: 100%;margin:0 !important" v-model="itemAmounts[row.id]"/>
</template>
<template v-slot:add="{ row }">
<a style="cursor: pointer;" @click="addToCart(row)">
<i class="fas fa-shopping-cart text-primary"></i>
</a>
</template>
</tt-table-crud>
</tt-card>
`, data() {
return {
window: window, itemAmounts: {}, shoppingCart: [], createOrderDialog: false, createOrderDialogData: {
deliveryMode: 'singleAddress',
deliveryAddressName: '',
deliveryAddressLine: '',
deliveryAddressPLZ: '',
deliveryAddressCity: '',
},
}
},
methods: {
async openOrderDialog() {
this.createOrderDialog = true;
},
async submitOrder() {
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseEShopOrder/createOrder`, {
shoppingCart: this.shoppingCart,
deliveryMode: this.createOrderDialogData.deliveryMode,
deliveryAddressName: this.createOrderDialogData.deliveryAddressName,
deliveryAddressLine: this.createOrderDialogData.deliveryAddressLine,
deliveryAddressPLZ: this.createOrderDialogData.deliveryAddressPLZ,
deliveryAddressCity: this.createOrderDialogData.deliveryAddressCity,
});
if (response.data.success) {
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
this.shoppingCart = [];
this.createOrderDialog = false;
} else {
this.window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message ||
'Ein Fehler ist aufgetreten');
}
},
addToCart(row) {
if (!this.itemAmounts[row.id] || this.itemAmounts[row.id] === 0) {
window.notify('error', 'Bitte geben Sie eine Menge ein.');
return;
}
// Check if Article is already in the shopping cart
if (this.shoppingCart.some(item => item.itemId === row.id)) {
window.notify('warning', `${row.title} ist bereits im Warenkorb.`);
return;
}
this.shoppingCart.push({itemId: row.id, title: row.title, amount: this.itemAmounts[row.id]});
window.notify('success', `${this.itemAmounts[row.id]}x ${row.title} wurde dem Warenkorb hinzugefügt.`);
}
},
})
@@ -0,0 +1,19 @@
Vue.component('warehouse-e-shop-order', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id">
<template v-slot:create="{ row }">
{{ new Date(row.create * 1000).toLocaleDateString() }}
</template>
</tt-table-crud>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null,
}
},
})
@@ -0,0 +1,45 @@
Vue.component('warehouse-history-modal', {
//language=Vue
template: `
<tt-modal :show.sync="showModal" title="History" :delete="false" :save="false"
@close="$emit('update:show', false)"
>
<tt-table
v-if="showModal"
:fetch-url="window['TT_CONFIG']['BASE_URL'] + '/getHistory?id=' + id"
:config="WarehouseHistoryTableConfig" small>
<template v-slot:create="{ row }">
{{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
</template>
</tt-table>
</tt-modal>
`,
props: {
show: { type: Boolean, default: false },
id: { type: Number, default: null },
},
data() {
return {
window: window,
showModal: false,
WarehouseHistoryTableConfig: {
key: 'WarehouseHistory',
tableHeader: 'Lager Historie',
headers: [
{text: "Zeit", key: "create", filter: false, sortable: false},
{text: "Feld", key: "columnHeader", filter: false, sortable: false},
{text: "Alter Wert", key: "old_value", filter: false, sortable: false},
{text: "Neuer Wert", key: "new_value", filter: false, sortable: false},
{text: "Geändert von", key: "user_name", filter: false, sortable: false},
],
}
}
},
watch: {
show(newVal) {
this.showModal = newVal
}
}
})
@@ -0,0 +1,13 @@
Vue.component('warehouse-item', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null,
}
},
})
@@ -0,0 +1,13 @@
Vue.component('warehouse-location', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null,
}
},
})
@@ -0,0 +1,12 @@
Vue.component('warehouse-order-recommendation', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null,
}
},
})