improved new features
This commit is contained in:
@@ -55,7 +55,9 @@ class WarehouseArticleController extends TTCrud {
|
||||
public static function updateCheapestPurchasePrice(int $id): void {
|
||||
$article = WarehouseArticleModel::get($id);
|
||||
if (!$article instanceof WarehouseArticleModel) throw new Exception("Invalid article type");
|
||||
if (($distributor = WarehouseArticleDistributorModel::getAll(['articleId' => $id], 1, 0, ['key' => 'purchasePrice', 'order' => 'ASC'])) && $article->cheapestPurchasePrice != $distributor[0]->purchasePrice)
|
||||
$distributor = WarehouseArticleDistributorModel::getAll(['articleId' => $id], 1, 0, ['key' => 'purchasePrice', 'order' => 'ASC']);
|
||||
if (count($distributor) == 0) WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => null]));
|
||||
else if ($article->cheapestPurchasePrice != $distributor[0]->purchasePrice)
|
||||
WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => $distributor[0]->purchasePrice]));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class WarehouseArticleDistributorController extends TTCrud {
|
||||
protected function checkExistingDistributorEntry($postData): bool {
|
||||
if (isset($postData['id'])) {
|
||||
$count = WarehouseArticleDistributorModel::count(['articleId' => $postData['articleId'],
|
||||
'distributorId' => $postData['articlePriceTypeId'],
|
||||
'distributorId' => $postData['distributorId'],
|
||||
'id' => $postData['id']]);
|
||||
|
||||
if ($count > 0) return true;
|
||||
@@ -66,6 +66,11 @@ class WarehouseArticleDistributorController extends TTCrud {
|
||||
WarehouseArticleController::updateSellPrices($postData['articleId']);
|
||||
}
|
||||
|
||||
protected function afterDelete($postData) {
|
||||
WarehouseArticleController::updateCheapestPurchasePrice($postData);
|
||||
WarehouseArticleController::updateSellPrices($postData);
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class WarehouseShippingNoteController extends TTCrud {
|
||||
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
|
||||
|
||||
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
|
||||
protected array $additionalJS = ['js/pages/WarehouseArticle/WarehouseArticleModal.js'];
|
||||
protected array $additionalHead = ['<link rel="manifest" href="/assets/pwa/shipping-note-manifest.json">'];
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
|
||||
|
||||
@@ -66,7 +66,7 @@ class TTCrud extends mfBaseController {
|
||||
|
||||
|
||||
protected function indexAction() {
|
||||
$this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']);
|
||||
$this->layout()->set('additionalJS', array_merge(['js/pages/WarehouseHistory/WarehouseHistoryModal.js'], $this->additionalJS ?? []));
|
||||
$pageName = (defined('BASEDIR') && file_exists(BASEDIR . "/public/js/pages/{$this->mod}/{$this->mod}.js"))
|
||||
? $this->mod
|
||||
: "DefaultCrudView";
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
254
public/js/pages/WarehouseArticle/WarehouseArticleModal.js
Normal file
254
public/js/pages/WarehouseArticle/WarehouseArticleModal.js
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -283,11 +283,18 @@ Vue.component('warehouse-order-modal', {
|
||||
>
|
||||
<template #form-actions-append>
|
||||
<tt-button
|
||||
v-if="!isNaN(parseInt($refs.positionsManager?.formData?.article))"
|
||||
text="Zum Artikel"
|
||||
sm
|
||||
additional-class="btn-outline-primary"
|
||||
@click="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle?showId=' + $refs.positionsManager.formData.article)"/>
|
||||
v-if="!isNaN(parseInt($refs.positionsManager?.formData?.article))"
|
||||
text="Zum Artikel"
|
||||
sm
|
||||
additional-class="btn-outline-primary"
|
||||
@click="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle?showId=' + $refs.positionsManager.formData.article)"/>
|
||||
|
||||
<tt-button
|
||||
v-if="!isNaN(parseInt($refs.positionsManager?.formData?.article)) && !isNaN(parseInt($refs.positionsManager?.formData?.distributorId))"
|
||||
text="Preis übern."
|
||||
sm
|
||||
additional-class="btn-outline-success"
|
||||
@click="updateArticlePriceForDistributor($refs.positionsManager.formData.article, $refs.positionsManager.formData.distributorId, $refs.positionsManager.formData.buyPrice)"/>
|
||||
</template>
|
||||
</tt-positions-manager>
|
||||
|
||||
@@ -457,6 +464,28 @@ Vue.component('warehouse-order-modal', {
|
||||
this.$refs.positionsManager.updateField('buyPrice', distributor.purchasePrice);
|
||||
}
|
||||
},
|
||||
async updateArticlePriceForDistributor(articleId, distributorId, buyPrice) {
|
||||
if (!articleId || !distributorId || !buyPrice) return;
|
||||
const res = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticleDistributor/get`, {
|
||||
filters: { articleId, distributorId }
|
||||
})
|
||||
const current = res.data.rows[0];
|
||||
if (current && current.purchasePrice === buyPrice) {
|
||||
window.notify('info', 'Preis ist bereits aktuell');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticleDistributor/update`, {
|
||||
...current,
|
||||
purchasePrice: buyPrice,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
window.notify('success', 'Preis erfolgreich aktualisiert');
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'order.positions': {
|
||||
|
||||
@@ -399,260 +399,6 @@ Vue.component('warehouse-shipping-note-see-through', {
|
||||
}
|
||||
});
|
||||
|
||||
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('warehouse-shipping-note', {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.tt-tooltip-wrapper {
|
||||
position: relative;
|
||||
display: inline-block; /* Or 'block' depending on your layout needs */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tt-tooltip-box {
|
||||
|
||||
@@ -12,6 +12,10 @@ Vue.component('tt-tooltip', {
|
||||
// The value must match one of these strings
|
||||
return ['top', 'bottom', 'left', 'right'].indexOf(value) !== -1;
|
||||
}
|
||||
},
|
||||
allowWrapping: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -23,7 +27,9 @@ Vue.component('tt-tooltip', {
|
||||
<div class="tt-tooltip-wrapper"
|
||||
@mouseenter="showTooltip = true"
|
||||
@mouseleave="showTooltip = false">
|
||||
<slot></slot> <div v-if="showTooltip" class="tt-tooltip-box" :class="['tt-tooltip-' + position]">
|
||||
<slot></slot> <div v-if="showTooltip" class="tt-tooltip-box"
|
||||
:style="{ whiteSpace: allowWrapping ? 'normal' : 'nowrap' }"
|
||||
:class="['tt-tooltip-' + position]">
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user