fixed multiple stuff

This commit is contained in:
Luca Haid
2025-04-23 22:41:28 +02:00
parent 092e9c558a
commit 82a2889e92
13 changed files with 296 additions and 735 deletions
@@ -0,0 +1,5 @@
.warehouse-article-prices > div {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr)) 72px;
gap: 10px;
}
@@ -1,369 +1,87 @@
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>
Vue.component('warehouse-article-prices', {
props: {id: {type: Number, required: true}},
template: `
<div>
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Lieferant hinzufügen</a>
<tt-card>
<h4>Artikelpreise</h4>
</tt-modal>
`, props: {
show: {type: Boolean, default: false}, id: {type: Number, default: null},
}, data() {
return {
window: window, showModal: false, rows: []
};
}, async mounted() {
<div class="warehouse-article-prices">
<div v-for="(price, index) in articlePrices" :key="index" class="article-price">
<div class="article-price-type">
<span v-if="price.isRobot" class="robot-icon"><i class="fa-solid fa-robot"></i></span>
{{ index }}
</div>
<tt-input sm v-model="price.priceMultiplier" label="Preisfaktor"/>
<tt-input sm v-model="price.priceOverride" label="Preisoverride"/>
}, methods: {
async fetchArticleDistributor() {
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/get', {filters: {articleId: this.id}});
<div>
<tt-button sm icon="fa-solid fa-trash" class="remove-price" additional-class="btn-danger"></tt-button>
<tt-button sm icon="fa-solid fa-save" class="save-price" additional-class="btn-primary"></tt-button>
</div>
this.rows = response.data.rows
}, addRow() {
this.rows.push({distributorId: undefined, price: null, externalArticleNumber: null});
}, async saveRow(index) {
// post to /WarehouseArticleDistributor/save with rows data and articleId
const row = this.rows[index];
</div>
</div>
</tt-card>
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();
}
</div>`,
data: () => ({
window,
articlePrices: [],
}),
async mounted() {
const [res1, res2] = await Promise.all([
axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}}),
axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePriceType/get')
]);
const prices = {};
res2.data.rows.forEach(t => prices[t.title] = {
isRobot: true,
articlePriceTypeId: t.id,
priceMultiplier: t.defaultPriceFactor,
priceOverride: null
});
res1.data.rows.forEach(p => {
const t = res2.data.rows.find(t => t.id === p.articlePriceTypeId);
if (t) prices[t.title] = {
isRobot: false,
articlePriceTypeId: p.articlePriceTypeId,
priceMultiplier: p.priceMultiplier || t.defaultPriceFactor,
priceOverride: p.priceOverride
};
});
this.articlePrices = prices;
}
})
// noinspection EqualityComparisonWithCoercionJS
Vue.component('warehouse-article', {
//language=Vue
template: `
<tt-card>
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"
@addToCart="addShoppingCartModal = true; addShoppingCartModalId = $event.id"
>
<tt-table-crud ref="table" @openHistory="historyModal = true; historyModalId = $event.id">
<template v-slot:cheapestsellprice="{ row }">
<template v-for="price in JSON.parse(row.cheapestSellPrice)">
<span v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">{{price.title}}: {{(price.price)}} €<br></span>
<span v-if="price && price.title === 'Verkauf'">{{(price.price)}} €</span>
</template>
</template>
<template v-slot:cheapestsellprice="{ row }">
<template v-for="price in JSON.parse(row.cheapestSellPrice)">
<span v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">{{price.title}}: <span
style="white-space:nowrap;"> {{(price.price)}} €</span><br></span>
<span v-else-if="price && price.title === 'Verkauf'">{{(price.price)}} €</span>
</template>
</template>
<template v-slot:cheapestPurchasePrice="{ row }">
<span>{{(row.cheapestPurchasePrice * row.sellPriceMultiplier).toFixed(2)}} €</span>
</template>
<template v-slot:modal-prepend="{ crudModalData }">
<warehouse-article-prices v-if="crudModalData.id" :id="crudModalData.id"/>
</template>
</tt-table-crud>
</tt-table-crud>
<tt-expandable-shopping-cart v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === true" :cart-items="shoppingCart" @submitOrder="prepareOrder"/>
<tt-modal :show.sync="addShoppingCartModal" title="Artikel zur Bestellung hinzufügen" :delete="false" @submit="addToShoppingCart"
@close="addShoppingCartModal = false">
<tt-input v-model="addShoppingCartModalCount" placeholder="Menge" type="number" sm></tt-input>
</tt-modal>
<tt-modal :show.sync="confirmOrderModal" title="Bestellung bestätigen" :delete="false"
save-text="Bestätigen" @submit="createOrder"
@close="confirmOrderModal = false; confirmOrderModalData = null">
<span>
Es werden Bestellungen an folgende Lieferanten gesendet:
</span>
<div v-for="(order, index) in confirmOrderModalData" :key="index">
<h4>{{order.distributor[0].name}} - {{order.orderAmount}} €
</h4>
<table class="table table-bordered">
<tr>
<th>Artikel</th>
<th>Menge</th>
<th>Preis</th>
<th>Summe</th>
</tr>
<tr v-for="(item, index) in order.orders" :key="index + item.id">
<td>{{item.title}}</td>
<td>{{item.amount}}</td>
<td>{{item.purchasePrice}} €</td>
<td>{{item.sum.toFixed(2)}} €</td>
</tr>
</table>
</div>
</tt-modal>
<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,
shoppingCart: [],
addShoppingCartModal: false,
addShoppingCartModalId: null,
addShoppingCartModalCount: '',
confirmOrderModal: false,
confirmOrderModalData: null,
}
}, methods: {
refreshTable() {
this.$refs.table.$refs.table.refreshTable();
}, async addToShoppingCart() {
if (this.addShoppingCartModalCount < 1) { // Check if amount is set
window.notify('error', 'Bitte geben Sie eine Menge ein.');
return;
}
if (this.shoppingCart.some(item => item.itemId === this.addShoppingCartModalId)) { // Check if same article is already in cart
window.notify('error', 'Artikel bereits im Warenkorb.');
return;
}
const response = await axios.get(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/getById?id=${this.addShoppingCartModalId}`);
this.shoppingCart.push({amount: parseInt(this.addShoppingCartModalCount), itemId: this.addShoppingCartModalId, title: response.data.title});
this.addShoppingCartModal = false;
this.addShoppingCartModalId = null;
this.addShoppingCartModalCount = '';
window.notify('success', 'Artikel erfolgreich hinzugefügt.');
}, async prepareOrder() {
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/prepareOrder`, this.shoppingCart);
this.confirmOrderModal = true;
this.confirmOrderModalData = response.data;
},
async createOrder() {
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder/createOrder`, this.confirmOrderModalData);
this.confirmOrderModal = false;
this.confirmOrderModalData = null;
this.shoppingCart = [];
window.notify(response.data.success ? 'success' : 'error', response.data.message);
setTimeout(() => {
window.location.href = `${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder`;
}, 2000);
}
},
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data: () => ({
window,
historyModal: false,
historyModalId: null,
}),
mounted() {
const table = this.$refs.table?.$refs?.table;
if (!table) return;
@@ -372,7 +90,7 @@ Vue.component('warehouse-article', {
const currentFilterId = table.filters?.id;
if ((showId && currentFilterId !== showId) || (!showId && currentFilterId)) {
table.filters = showId ? { id: showId } : {};
table.filters = showId ? {id: showId} : {};
table.refreshTable();
}
}
@@ -297,7 +297,11 @@ Vue.component('warehouse-shipping-note-see-through', {
</div>
<div v-if="activeTab === 'Editieren'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
<warehouse-shipping-note-modal v-if="currentRow" :id="currentRow.id" @close="fetchData" ref="modal"/>
<warehouse-shipping-note-modal v-if="currentRow" :id="currentRow.id" @close="fetchData" ref="modal">
<template v-slot:footer-prepend v-if="wantedState === 'new'">
<button class="btn btn-warning" @click="saveAndSetToProgress">Speichern und in Bearbeitung setzen</button>
</template>
</warehouse-shipping-note-modal>
</div>
<div v-if="activeTab === 'Logs'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
<warehouse-shipping-note-logs :shipping-note-id="currentRow.id" :key="'logs' + logModalKey"/>
@@ -368,6 +372,11 @@ Vue.component('warehouse-shipping-note-see-through', {
}
this.loading = false;
},
async saveAndSetToProgress() {
await this.$refs.modal.submit('in_progress');
await new Promise(resolve => setTimeout(resolve, 50));
await this.fetchData();
}
},
mounted() {
@@ -381,7 +390,7 @@ Vue.component('warehouse-shipping-note', {
template: `
<tt-card>
<warehouse-shipping-note-see-through
@close="shippingNoteSeeThrough = false"
@close="shippingNoteSeeThrough = false;$refs.table.$refs.table.refreshTable()"
v-if="shippingNoteSeeThrough !== false"
:wanted-state="shippingNoteSeeThrough"
@status_to_progress="changeStatus($event.id, 'in_progress')"
@@ -115,7 +115,7 @@ Vue.component('warehouse-shipping-note-modal', {
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">
<hr>
<tt-autocomplete v-model="shippingNote.billingAddressId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
<tt-autocomplete v-model="shippingNote.type" :items="availableTypes" label="Typ" sm row return-text/>
<tt-autocomplete v-model="shippingNote.type" :items="availableTypes" label="Typ" sm row return-text case-insensitive/>
</template>
<tt-textarea v-model="shippingNote.note" label="Art der Arbeit" sm row/>
@@ -138,9 +138,13 @@ Vue.component('warehouse-shipping-note-modal', {
<div v-else class="text-danger text-center font-weight-bolder font-16">Bitte Lieferadresse eingeben oder nach Kunden suchen</div>
</div>
<template v-slot:footer-prepend v-if="id !== 'create'">
<template v-slot:footer-prepend v-if="id !== 'create' && !$slots['footer-prepend']">
<button class="btn btn-info" @click="$emit('open-signing-modal', id)">Unterschreiben</button>
</template>
<template v-slot:footer-prepend v-else>
<slot name="footer-prepend"/>
</template>
</tt-modal>
`,
@@ -177,7 +181,7 @@ Vue.component('warehouse-shipping-note-modal', {
}
},
methods: {
async submit() {
async submit(newStatus = null) {
this.loading = true;
if (!this.shippingNote.positions.length && !this.shippingNote.hoursEntries.length) {
this.loading = false;
@@ -188,6 +192,10 @@ Vue.component('warehouse-shipping-note-modal', {
this.shippingNote.type = this.availableTypes.find(t => t.name === this.shippingNote.type).value;
}
if (newStatus !== null) {
this.shippingNote.status = newStatus;
}
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/${this.id === 'create' ? 'create' : 'update'}`, this.shippingNote);
if ((this.id !== 'create' && response.data.success === true) || response.data.success === true) this.$emit('close');