Files
thetool/public/js/pages/WarehouseArticle/WarehouseArticle.js
2025-07-10 13:07:39 +02:00

201 lines
9.9 KiB
JavaScript

async function handleApiResponse(responsePromise) {
const res = await responsePromise;
if (!res.data.success) {
const errors = res.data.errors;
const errorMessage = Array.isArray(errors) ? errors.join(', ') : Object.values(errors).join(', ');
return window.notify('error', `Fehler: ${errorMessage}`);
}
window.notify('success', res.data.message || 'Erfolgreich');
window.dispatchEvent(new Event('refreshTable'));
}
Vue.component('warehouse-article-prices', {
props: {id: {type: Number, required: true}},
template: `
<tt-card>
<h4 style="text-align: center">Artikelpreise überschreiben</h4>
<div class="warehouse-article-prices">
<div v-for="(price, typeTitle) in articlePrices" :key="typeTitle">
<div style="align-self: center;">
<i v-if="price.isRobot" class="fa-solid fa-robot"></i> {{ typeTitle }}
</div>
<tt-input sm v-model="price.priceMultiplier" label="Preisfaktor" @input="price.priceOverride = null;price.pendingChanges = true"/>
<tt-input sm v-model="price.priceOverride" label="Preis" @input="price.priceMultiplier = null;price.pendingChanges = true"/>
<div style="align-self: end;display: flex;align-items: center;">
<tt-button sm icon="fa-solid fa-save" additional-class="btn-primary" @click="savePrice(price)"/>
<tt-button sm icon="fa-solid fa-trash" additional-class="btn-danger" v-if="!price.isRobot" @click="deletePrice(price)"/>
<i v-if="price.pendingChanges" class="fa-solid fa-triangle-exclamation ml-1 text-warning" style="font-size: 28px" title="Dieser Preis wurde noch nicht gespeichert"></i>
</div>
</div>
</div>
</tt-card>
`,
data: () => ({window, articlePrices: {}}),
async mounted() {
await this.fetchArticlePrices();
},
methods: {
async fetchArticlePrices() {
const [pricesRes, typesRes] = 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 = {};
typesRes.data.rows.forEach(type => prices[type.title] = {
isRobot: true,
articlePriceTypeId: type.id,
priceMultiplier: type.defaultPriceFactor,
priceOverride: null
});
pricesRes.data.rows.forEach(pData => {
const type = typesRes.data.rows.find(t => t.id === pData.articlePriceTypeId);
if (type) prices[type.title] = {
id: pData.id,
isRobot: false,
pendingChanges: false,
articlePriceTypeId: pData.articlePriceTypeId,
priceMultiplier: pData.priceMultiplier,
priceOverride: pData.priceOverride
};
});
this.articlePrices = prices;
},
async savePrice(price) {
const payload = {
articleId: this.id,
articlePriceTypeId: price.articlePriceTypeId,
priceMultiplier: price.priceMultiplier ? parseFloat(price.priceMultiplier.toString().replace(',', '.')) : null,
priceOverride: price.priceOverride ? parseFloat(price.priceOverride.toString().replace(',', '.')) : null
};
if (price.isRobot) {
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/create`, payload));
await this.fetchArticlePrices();
} else {
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/update`, {id: price.id, ...payload}));
await this.fetchArticlePrices();
}
},
async deletePrice(price) {
const payload = {id: price.id, articleId: this.id, articlePriceTypeId: price.articlePriceTypeId}
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/delete`, payload));
await this.fetchArticlePrices();
}
}
});
// warehouse-article-distributor.vue.js
Vue.component('warehouse-article-distributor', {
props: {id: {type: Number, required: true}},
template: `
<tt-card>
<h4 style="text-align: center">Lieferanten für diesen Artikel</h4>
<tt-autocomplete :api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseDistributor/autocomplete'"
v-model="newDistributorId" label="Neuen Lieferant hinzufügen"/>
<div class="warehouse-article-distributor">
<div v-for="(distributor, index) in articleDistributors" :key="distributor.id || ('new-' + index)">
<tt-resolver style="align-self: center;" reference="WarehouseDistributor"
:value="distributor.distributorId"></tt-resolver>
<tt-input sm v-model="distributor.externalArticleNumber" label="Externe Artikelnummer" @input="distributor.pendingChanges = true"/>
<tt-input sm v-model="distributor.purchasePrice" label="Einkaufspreis" @input="distributor.pendingChanges = true"/>
<div style="align-self: end; display: flex;align-items: center;">
<tt-button sm icon="fa-solid fa-save" additional-class="btn-primary"
@click="saveDistributor(distributor)"/>
<tt-button sm icon="fa-solid fa-trash" additional-class="btn-danger" v-if="distributor.id"
@click="deleteDistributor(distributor.id)"/>
<i v-if="distributor.pendingChanges || !distributor.id" class="fa-solid fa-triangle-exclamation ml-1 text-warning"
style="font-size: 28px" title="Dieser Preis wurde noch nicht gespeichert"></i>
</div>
</div>
</div>
</tt-card>
`,
data: () => ({window, articleDistributors: [], newDistributorId: null}),
async mounted() {
await this.fetchArticleDistributors();
},
methods: {
async fetchArticleDistributors() {
const res = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/get`, {filters: {articleId: this.id}});
this.articleDistributors = res.data.rows;
},
async saveDistributor(distributor) {
delete distributor.pendingChanges;
distributor.purchasePrice = distributor.purchasePrice ? parseFloat(distributor.purchasePrice.toString().replace(',', '.')) : null;
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${distributor.id ? 'update' : 'create'}`, distributor));
await this.fetchArticleDistributors();
},
async deleteDistributor(distributorId) {
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/delete`, {id: distributorId, articleId: this.id}));
await this.fetchArticleDistributors();
}
},
watch: {
newDistributorId(newId) {
if (newId) {
if (!this.articleDistributors.some(d => d.distributorId === newId)) {
this.articleDistributors.push({
articleId: this.id,
distributorId: newId,
externalArticleNumber: null,
purchasePrice: null
});
}
this.$nextTick(() => {
this.newDistributorId = null;
});
}
}
}
});
// warehouse-article.vue.js
Vue.component('warehouse-article', {
template: `
<tt-card>
<tt-table-crud ref="table" @openHistory="historyModalId = $event.id; historyModal = true">
<template v-slot:cheapestsellprice="{ row }">
<template v-for="price in JSON.parse(row.cheapestSellPrice || '[]')">
<span style="white-space:nowrap;" v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN']">{{ price.title }}: <span
>{{ price.price }} €</span><br></span>
<span v-else-if="price && price.title === 'Verkauf'">{{ price.price }} €</span>
</template>
</template>
<template v-slot:description="{ row }">
<tt-tooltip
allow-wrapping
v-if="row.description.length > 45"
:text="row.description" position="top">
<span v-if="row.description.length > 45">{{ row.description.substring(0, 45) }}...</span>
</tt-tooltip>
<span v-else>{{ row.description }}</span>
</template>
<template v-slot:modal-prepend="{ crudModalData }">
<warehouse-article-prices v-if="crudModalData.id" :id="crudModalData.id"/>
<warehouse-article-distributor v-if="crudModalData.id" :id="crudModalData.id"/>
</template>
</tt-table-crud>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`,
data: () => ({window, historyModal: false, historyModalId: null, articleTest: null}),
mounted() {
const table = this.$refs.table?.$refs?.table;
if (!table) return;
const showId = new URLSearchParams(window.location.search).get('showId');
if (showId && (!table.filters || table.filters.id !== showId)) {
table.filters = {...table.filters, id: showId};
table.refreshTable();
} else if (!showId && table.filters?.id) {
delete table.filters.id;
if (Object.keys(table.filters).length === 0) table.filters = {};
table.refreshTable();
}
window.addEventListener('refreshTable', () => {
table.refreshTable();
});
}
});