improved new features

This commit is contained in:
Luca Haid
2025-04-29 14:09:36 +02:00
parent 21f65aa905
commit a859bef97e
10 changed files with 338 additions and 279 deletions
@@ -14,12 +14,12 @@ Vue.component('warehouse-article-prices', {
<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"/>
<tt-input sm v-model="price.priceOverride" label="Preis" @input="price.priceMultiplier = null"/>
<div style="align-self: end;">
<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)"/>
<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>
@@ -47,6 +47,7 @@ Vue.component('warehouse-article-prices', {
if (type) prices[type.title] = {
id: pData.id,
isRobot: false,
pendingChanges: false,
articlePriceTypeId: pData.articlePriceTypeId,
priceMultiplier: pData.priceMultiplier,
priceOverride: pData.priceOverride
@@ -58,8 +59,8 @@ Vue.component('warehouse-article-prices', {
const payload = {
articleId: this.id,
articlePriceTypeId: price.articlePriceTypeId,
priceMultiplier: price.priceMultiplier,
priceOverride: price.priceOverride
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));
@@ -89,13 +90,16 @@ Vue.component('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"/>
<tt-input sm v-model="distributor.purchasePrice" label="Einkaufspreis"/>
<div style="align-self: end;">
<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)"/>
@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>
@@ -111,11 +115,13 @@ Vue.component('warehouse-article-distributor', {
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(distributor) {
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/delete`, distributor));
async deleteDistributor(distributorId) {
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/delete`, {id: distributorId}));
await this.fetchArticleDistributors();
}
},
@@ -145,11 +151,22 @@ Vue.component('warehouse-article', {
<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 v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN']">{{ price.title }}: <span
style="white-space:nowrap;">{{ price.price }} €</span><br></span>
<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"/>
@@ -0,0 +1,254 @@
Vue.component('warehouse-article-modal', {
template: `
<tt-modal :show="true" :save="false" :delete="false" @update:show="$emit('close')"
title="Artikel Suchen">
<tt-card>
<div v-if="!isLoadingCategories" :style="{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '10px',
padding: '10px',
marginBottom: '15px',
borderBottom: '1px solid #eee'
}">
<div v-for="category in categories"
:key="category.id"
@click="selectCategory(category)"
:style="getCategoryStyle(category)"
style="
padding: '10px 15px';
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
background-color: #f9f9f9;
font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
:title="category.name"
@mouseover="hoverCategory($event, true)"
@mouseleave="hoverCategory($event, false)">
{{ category.name }}
</div>
</div>
<div v-else style="text-align: center; padding: 20px; color: #777;">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Lade Kategorien...
</div>
<div v-if="selectedCategory" style="padding: 0 10px 15px 10px;">
<tt-input
label="Artikel suchen"
placeholder="Titel, Artikelnummer, Beschreibung..."
v-model="searchTerm"
:sm="true"
:row="false"
type="search"
hint="Sucht in Titel, Artikelnummer und Beschreibung."
/>
</div>
<div v-if="selectedCategory" style="padding: 0 10px 10px 10px; min-height: 150px; position: relative;">
<div v-if="isLoadingArticles" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.7); display: flex; justify-content: center; align-items: center; z-index: 10;">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span style="margin-left: 10px;">Lade Artikel...</span>
</div>
<div v-if="!isLoadingArticles && filteredArticles.length > 0" :style="{ display: 'grid', gridTemplateColumns: '1fr', gap: '8px' }">
<div v-for="article in filteredArticles"
:key="article.id"
@click="selectArticle(article)"
style="
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
background-color: #fff;
transition: background-color 0.15s ease;
"
@mouseover="event => event.currentTarget.style.backgroundColor='#f5f5f5'"
@mouseleave="event => event.currentTarget.style.backgroundColor='#fff'">
<div>
<strong style="font-size: 0.95em;">{{ article.title }}</strong>
<span style="font-size: 0.85em; color: #666; margin-left: 10px;">({{ article.articleNumber }})</span>
</div>
<div v-if="article.description" style="font-size: 0.85em; color: #555; margin-top: 3px;">
{{ article.description }}
</div>
</div>
</div>
<div v-if="!isLoadingArticles && articles.length > 0 && filteredArticles.length === 0" style="color: #777; text-align: center; padding: 20px;">
Keine Artikel entsprechen Ihrer Suche nach "{{ searchTerm }}".
</div>
<div v-if="!isLoadingArticles && articles.length === 0 && !isLoadingArticles" style="color: #777; text-align: center; padding: 20px;">
Keine Artikel in der ausgewählten Kategorie gefunden.
</div>
</div>
</tt-card>
</tt-modal>
`,
data() {
return {
window: window,
isLoadingCategories: false, // Added loading state for categories
categories: [],
selectedCategory: null,
articles: [],
searchTerm: '',
isLoadingArticles: false,
}
},
computed: {
filteredArticles() {
if (!this.searchTerm) {
return this.articles;
}
const lowerSearchTerm = this.searchTerm.toLowerCase();
// Ensure articles is an array before filtering
if (!Array.isArray(this.articles)) {
console.warn('Attempted to filter non-array articles:', this.articles);
return [];
}
return this.articles.filter(article => {
const titleMatch = article.title?.toLowerCase().includes(lowerSearchTerm);
const articleNumberMatch = article.articleNumber?.toLowerCase().includes(lowerSearchTerm);
const descriptionMatch = article.description?.toLowerCase().includes(lowerSearchTerm);
return titleMatch || articleNumberMatch || descriptionMatch;
});
}
},
methods: {
async selectCategory(category) {
if (this.selectedCategory && this.selectedCategory.id === category.id) {
return;
}
this.selectedCategory = category;
this.searchTerm = '';
this.articles = [];
console.log('Selected Category:', this.selectedCategory);
await this.fetchArticles(category.id);
},
async fetchArticles(categoryId) {
if (!categoryId) return;
this.isLoadingArticles = true;
try {
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseArticle/getAll', {
filters: {
category_id: categoryId
}
});
// Robust check: Ensure response.data is an array
if (Array.isArray(response.data)) {
this.articles = response.data;
} else {
console.warn('Fetched articles data is not an array:', response.data);
this.articles = []; // Set to empty array if not valid
}
console.log('Fetched Articles:', this.articles);
} catch (error) {
console.error("Error fetching articles:", error);
this.articles = [];
} finally {
this.isLoadingArticles = false;
}
},
selectArticle(article) {
console.log('Selected Article:', article);
this.$emit('article-selected', article);
this.$emit('close');
},
getCategoryStyle(category) {
const style = {};
if (this.selectedCategory && this.selectedCategory.id === category.id) {
style.backgroundColor = '#d0e0ff';
style.borderColor = '#a0c0ff';
style.fontWeight = 'bold';
style.boxShadow = '0 0 5px rgba(0, 100, 255, 0.3)';
}
return style;
},
hoverCategory(event, isHovering) {
// Call findCategoryId safely
const categoryId = this.findCategoryId(event.target.innerText);
// Guard: Only proceed if categoryId was found
if (categoryId === null) {
// console.warn('Could not find category ID for:', event.target.innerText);
return; // Stop if ID couldn't be found (e.g., categories not loaded yet)
}
// Rest of the hover logic...
if (!event.target.dataset.categoryId) {
event.target.dataset.categoryId = categoryId;
}
if (!this.selectedCategory || this.selectedCategory.id != categoryId) {
if (isHovering) {
event.target.style.backgroundColor = '#e9e9e9';
event.target.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
} else {
event.target.style.backgroundColor = '#f9f9f9';
event.target.style.boxShadow = 'none';
}
} else {
if (!isHovering) {
event.target.style.boxShadow = this.getCategoryStyle(this.selectedCategory).boxShadow || 'none';
event.target.style.backgroundColor = this.getCategoryStyle(this.selectedCategory).backgroundColor ||'#d0e0ff';
}
}
},
findCategoryId(name) {
// **** GUARD ADDED HERE ****
// Check if categories is an array and has items before using .find()
if (!Array.isArray(this.categories) || this.categories.length === 0) {
// console.warn('findCategoryId called before categories array is ready or populated.');
return null; // Return null if categories are not ready
}
// Now it's safe to use .find()
const found = this.categories.find(cat => cat && cat.name === name); // Added check for cat existence
return found ? found.id : null;
}
},
async mounted() {
this.isLoadingCategories = true; // Set loading true before fetch
try {
const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseCategory/getAll');
console.log('Raw category response.data:', response.data); // Log the raw response
console.log('Is response.data an array?', Array.isArray(response.data)); // Check if it's an array
// **** ROBUST ASSIGNMENT ****
// Ensure we assign an array, even if the response isn't one.
if (Array.isArray(response.data)) {
this.categories = response.data;
} else if (response.data && Array.isArray(response.data.data)) {
// Example: Handle common case where data is nested like { data: [...] }
console.log('Assigning categories from response.data.data');
this.categories = response.data.data;
}
else {
console.warn('Categories response.data is not an array and not handled structure:', response.data);
this.categories = []; // Default to empty array if response is unexpected
}
console.log('Assigned this.categories:', this.categories);
} catch (error) {
console.error("Error fetching categories:", error);
this.categories = []; // Ensure it's an array on error
} finally {
this.isLoadingCategories = false; // Set loading false after fetch/error
}
}
})