Merge branch 'Warehouse/improve3' into 'master'

improved new features

See merge request fronk/thetool!1280
This commit is contained in:
Luca Haid
2025-04-29 12:11:33 +00:00
10 changed files with 338 additions and 279 deletions

View File

@@ -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]));
}

View File

@@ -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);

View File

@@ -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.',

View File

@@ -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";

View File

@@ -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"/>

View 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
}
}
})

View File

@@ -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': {

View File

@@ -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', {

View File

@@ -1,7 +1,6 @@
.tt-tooltip-wrapper {
position: relative;
display: inline-block; /* Or 'block' depending on your layout needs */
cursor: pointer;
}
.tt-tooltip-box {

View File

@@ -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>