fixed article modal
This commit is contained in:
@@ -28,8 +28,7 @@
|
||||
|
||||
.positions-manager .form-container [class*="tt-input"],
|
||||
.positions-manager .form-container [class*="tt-checkbox"] {
|
||||
flex: 1 1 200px; /* Flexible width with a minimum of 200px */
|
||||
min-width: 200px;
|
||||
flex: 1 1; /* Flexible width with a minimum of 200px */
|
||||
}
|
||||
|
||||
.positions-manager .form-container .btn {
|
||||
|
||||
@@ -225,6 +225,7 @@ td {
|
||||
.width-80 {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.width-120 {
|
||||
width: 120px;
|
||||
}
|
||||
@@ -254,7 +255,6 @@ td {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*TODO: remove this*/
|
||||
/* Styles for the component */
|
||||
.tt-expandable-shopping-cart {
|
||||
@@ -313,7 +313,7 @@ td {
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .cart-content {
|
||||
padding-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .cart-content h3 {
|
||||
@@ -359,10 +359,21 @@ td {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-loading {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* tt-input-article */
|
||||
.tt-input-article-wrapper .autocomplete {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tt-input-article-wrapper button[type=button].btn-link.position-absolute {
|
||||
right: 30px !important;
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -76,7 +76,19 @@ Vue.component('tt-positions-manager',
|
||||
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
|
||||
sm
|
||||
/>
|
||||
<tt-textarea
|
||||
<tt-input-article
|
||||
v-if="field.type === 'input-article'"
|
||||
:label="field.label"
|
||||
:emit-display-value="field.emitDisplayValue || false"
|
||||
v-model="formData[key]"
|
||||
@input="$emit('updateField-' + key, $event)"
|
||||
@displayValue="$emit('updateField-' + key + '_text', $event)"
|
||||
:ref="'article-' + key"
|
||||
:api-url="field.apiUrl"
|
||||
:field="field"
|
||||
sm
|
||||
/>
|
||||
<tt-textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
:label="field.label"
|
||||
v-model="formData[key]"
|
||||
|
||||
Reference in New Issue
Block a user