diff --git a/application/WarehouseArticle/WarehouseArticleController.php b/application/WarehouseArticle/WarehouseArticleController.php
index 38fbc9704..bf4d1c3bd 100644
--- a/application/WarehouseArticle/WarehouseArticleController.php
+++ b/application/WarehouseArticle/WarehouseArticleController.php
@@ -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]));
}
diff --git a/application/WarehouseArticleDistributor/WarehouseArticleDistributorController.php b/application/WarehouseArticleDistributor/WarehouseArticleDistributorController.php
index 73aa7ca14..d001982d8 100644
--- a/application/WarehouseArticleDistributor/WarehouseArticleDistributorController.php
+++ b/application/WarehouseArticleDistributor/WarehouseArticleDistributorController.php
@@ -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);
diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteController.php b/application/WarehouseShippingNote/WarehouseShippingNoteController.php
index b3c8b3810..d9de1f636 100644
--- a/application/WarehouseShippingNote/WarehouseShippingNoteController.php
+++ b/application/WarehouseShippingNote/WarehouseShippingNoteController.php
@@ -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 = [''];
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
diff --git a/lib/TTCrud/TTCrud.php b/lib/TTCrud/TTCrud.php
index 61e84108e..b76bbad8e 100644
--- a/lib/TTCrud/TTCrud.php
+++ b/lib/TTCrud/TTCrud.php
@@ -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";
diff --git a/public/js/pages/WarehouseArticle/WarehouseArticle.js b/public/js/pages/WarehouseArticle/WarehouseArticle.js
index 521f4b4a8..97061783a 100644
--- a/public/js/pages/WarehouseArticle/WarehouseArticle.js
+++ b/public/js/pages/WarehouseArticle/WarehouseArticle.js
@@ -14,12 +14,12 @@ Vue.component('warehouse-article-prices', {
{{ typeTitle }}
-
-
-
@@ -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', {
-
-
-
+
+
+
+ @click="deleteDistributor(distributor.id)"/>
+
+
@@ -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', {
- {{ price.title }}: {{ price.price }} €
+ {{ price.title }}: {{ price.price }} €
{{ price.price }} €
+
+
+
+ {{ row.description.substring(0, 45) }}...
+
+ {{ row.description }}
+
+
diff --git a/public/js/pages/WarehouseArticle/WarehouseArticleModal.js b/public/js/pages/WarehouseArticle/WarehouseArticleModal.js
new file mode 100644
index 000000000..10f564f9b
--- /dev/null
+++ b/public/js/pages/WarehouseArticle/WarehouseArticleModal.js
@@ -0,0 +1,254 @@
+Vue.component('warehouse-article-modal', {
+ template: `
+
+
+
+
+ {{ category.name }}
+
+
+
+
+ Lade Kategorien...
+
+
+
+
+
+
+
+
+
+
+ Lade Artikel...
+
+
+
+
event.currentTarget.style.backgroundColor='#f5f5f5'"
+ @mouseleave="event => event.currentTarget.style.backgroundColor='#fff'">
+
+ {{ article.title }}
+ ({{ article.articleNumber }})
+
+
+ {{ article.description }}
+
+
+
+
+
+ Keine Artikel entsprechen Ihrer Suche nach "{{ searchTerm }}".
+
+
+
+ Keine Artikel in der ausgewählten Kategorie gefunden.
+
+
+
+
+
+ `,
+ 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
+ }
+ }
+})
diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js
index ed5e81b55..41c8e3aef 100644
--- a/public/js/pages/WarehouseOrder/WarehouseOrder.js
+++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js
@@ -283,11 +283,18 @@ Vue.component('warehouse-order-modal', {
>
+ 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)"/>
+
+
@@ -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': {
diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
index c7e54b930..3b953f031 100644
--- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
+++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
@@ -399,260 +399,6 @@ Vue.component('warehouse-shipping-note-see-through', {
}
});
-Vue.component('warehouse-article-modal', {
- template: `
-
-
-
-
- {{ category.name }}
-
-
-
-
- Lade Kategorien...
-
-
-
-
-
-
-
-
-
-
- Lade Artikel...
-
-
-
-
event.currentTarget.style.backgroundColor='#f5f5f5'"
- @mouseleave="event => event.currentTarget.style.backgroundColor='#fff'">
-
- {{ article.title }}
- ({{ article.articleNumber }})
-
-
- {{ article.description }}
-
-
-
-
-
- Keine Artikel entsprechen Ihrer Suche nach "{{ searchTerm }}".
-
-
-
- Keine Artikel in der ausgewählten Kategorie gefunden.
-
-
-
-
-
- `,
- 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', {
diff --git a/public/plugins/vue/tt-components/css/tt-tooltip.css b/public/plugins/vue/tt-components/css/tt-tooltip.css
index 38f74f779..f33131e51 100644
--- a/public/plugins/vue/tt-components/css/tt-tooltip.css
+++ b/public/plugins/vue/tt-components/css/tt-tooltip.css
@@ -1,7 +1,6 @@
.tt-tooltip-wrapper {
position: relative;
display: inline-block; /* Or 'block' depending on your layout needs */
- cursor: pointer;
}
.tt-tooltip-box {
diff --git a/public/plugins/vue/tt-components/tt-tooltip.js b/public/plugins/vue/tt-components/tt-tooltip.js
index 308236fbc..e4b64644f 100644
--- a/public/plugins/vue/tt-components/tt-tooltip.js
+++ b/public/plugins/vue/tt-components/tt-tooltip.js
@@ -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', {