363 lines
15 KiB
JavaScript
363 lines
15 KiB
JavaScript
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
|
|
}
|
|
}
|
|
})
|
|
|
|
|
|
|
|
Vue.component('tt-input-article', {
|
|
template: `
|
|
<div class="tt-input-article-wrapper" :style="wrapperStyle">
|
|
<tt-autocomplete
|
|
:value="value"
|
|
:label="label"
|
|
:api-url="fullApiUrl"
|
|
:placeholder="placeholder"
|
|
:items="items"
|
|
:sm="sm"
|
|
:row="row"
|
|
:return-text="returnText"
|
|
:emit-display-value="emitDisplayValue"
|
|
:case-insensitive="caseInsensitive"
|
|
ref="autocomplete"
|
|
@input="updateValue"
|
|
@displayValue="updateDisplayValue"
|
|
>
|
|
<template v-slot:append>
|
|
<tt-button
|
|
style="max-width: 32px; margin-left: 5px;"
|
|
sm
|
|
text=""
|
|
@click="openArticleModal"
|
|
additional-class="btn-outline-primary"
|
|
icon="fa fa-search"/>
|
|
</template>
|
|
</tt-autocomplete>
|
|
|
|
<!-- Article Search Modal -->
|
|
<warehouse-article-modal
|
|
v-if="showArticleModal"
|
|
@close="showArticleModal = false"
|
|
@article-selected="handleArticleSelected"
|
|
></warehouse-article-modal>
|
|
</div>
|
|
`,
|
|
props: {
|
|
value: { type: [String, Number] },
|
|
label: { type: String, required: false },
|
|
apiUrl: { type: String, default: '/WarehouseArticle/autoComplete' },
|
|
placeholder: { type: String, default: '' },
|
|
items: { type: Array, default: () => [] },
|
|
sm: { type: Boolean, default: true },
|
|
row: { type: Boolean, default: false },
|
|
returnText: { type: Boolean, default: false },
|
|
emitDisplayValue: { type: Boolean, default: true },
|
|
caseInsensitive: { type: Boolean, default: false },
|
|
field: { type: Object, default: () => ({}) },
|
|
},
|
|
data() {
|
|
return {
|
|
window,
|
|
showArticleModal: false
|
|
};
|
|
},
|
|
computed: {
|
|
fullApiUrl() {
|
|
// Ensure the API URL has the base path
|
|
return this.apiUrl.startsWith('/') ?
|
|
window.TT_CONFIG.BASE_PATH + this.apiUrl :
|
|
this.apiUrl;
|
|
},
|
|
wrapperStyle() {
|
|
return this.field && this.field.style ? this.field.style : '';
|
|
}
|
|
},
|
|
methods: {
|
|
openArticleModal() {
|
|
this.showArticleModal = true;
|
|
},
|
|
|
|
handleArticleSelected(article) {
|
|
if (!article) return;
|
|
|
|
// Update the value (article ID)
|
|
this.$emit('input', article.id);
|
|
|
|
// Update the display text (article title)
|
|
if (this.emitDisplayValue) {
|
|
this.$emit('displayValue', article.title);
|
|
}
|
|
|
|
// Force the autocomplete component to update
|
|
if (this.$refs.autocomplete && typeof this.$refs.autocomplete.updateDisplayValue === 'function') {
|
|
this.$refs.autocomplete.displayValue = article.title;
|
|
}
|
|
},
|
|
|
|
updateValue(val) {
|
|
this.$emit('input', val);
|
|
},
|
|
|
|
updateDisplayValue(val) {
|
|
if (this.emitDisplayValue) {
|
|
this.$emit('displayValue', val);
|
|
}
|
|
},
|
|
|
|
clear() {
|
|
if (this.$refs.autocomplete && typeof this.$refs.autocomplete.clear === 'function') {
|
|
this.$refs.autocomplete.clear();
|
|
}
|
|
}
|
|
}
|
|
}); |