Merge branch 'WarehouseArticle/change-modal' into 'master'
fixed warehouse article See merge request fronk/thetool!1968
This commit is contained in:
@@ -56,12 +56,16 @@ class WarehouseArticleController extends TTCrud {
|
|||||||
protected function beforeCreate() {
|
protected function beforeCreate() {
|
||||||
if (!in_array($this->user->id, [2, 5, 6, 145, 14]))
|
if (!in_array($this->user->id, [2, 5, 6, 145, 14]))
|
||||||
self::sendError("Sie haben keine Berechtigung, Artikel zu erstellen.");
|
self::sendError("Sie haben keine Berechtigung, Artikel zu erstellen.");
|
||||||
|
|
||||||
|
$this->validateArticleNumber($_POST);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function beforeUpdate($postData): bool {
|
protected function beforeUpdate($postData): bool {
|
||||||
if (!in_array($this->user->id, [2, 5, 6, 145, 14]))
|
if (!in_array($this->user->id, [2, 5, 6, 145, 14]))
|
||||||
self::sendError("Sie haben keine Berechtigung, Artikel zu bearbeiten.");
|
self::sendError("Sie haben keine Berechtigung, Artikel zu bearbeiten.");
|
||||||
|
|
||||||
|
$this->validateArticleNumber($postData, $postData['id'] ?? null);
|
||||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,38 @@ class WarehouseArticleController extends TTCrud {
|
|||||||
self::updateSellPrices($postData['id']);
|
self::updateSellPrices($postData['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate article number for duplicates and correct category prefix
|
||||||
|
*/
|
||||||
|
private function validateArticleNumber(array $postData, ?int $excludeId = null): void {
|
||||||
|
$articleNumber = $postData['articleNumber'] ?? '';
|
||||||
|
$categoryId = $postData['category_id'] ?? null;
|
||||||
|
|
||||||
|
if (empty($articleNumber)) {
|
||||||
|
self::sendError("Artikelnummer ist erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate article number
|
||||||
|
$existingArticles = WarehouseArticleModel::getAll(['articleNumber' => $articleNumber]);
|
||||||
|
foreach ($existingArticles as $existing) {
|
||||||
|
if ($excludeId === null || $existing->id != $excludeId) {
|
||||||
|
self::sendError("Artikelnummer '{$articleNumber}' existiert bereits (Artikel ID: {$existing->id}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate category prefix
|
||||||
|
if ($categoryId) {
|
||||||
|
$category = WarehouseCategory::get($categoryId);
|
||||||
|
if ($category && $category->articleNumberPrefix) {
|
||||||
|
$expectedPrefix = $category->articleNumberPrefix;
|
||||||
|
$articlePrefix = substr($articleNumber, 0, strlen($expectedPrefix));
|
||||||
|
if ($articlePrefix !== $expectedPrefix) {
|
||||||
|
self::sendError("Artikelnummer muss mit dem Kategorie-Prefix '{$expectedPrefix}' beginnen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function updateSellPrices(int $id): void { // Added return type hint
|
public static function updateSellPrices(int $id): void { // Added return type hint
|
||||||
$a = WarehouseArticleModel::get($id);
|
$a = WarehouseArticleModel::get($id);
|
||||||
if (!$a instanceof WarehouseArticleModel) throw new Exception("Invalid article type");
|
if (!$a instanceof WarehouseArticleModel) throw new Exception("Invalid article type");
|
||||||
|
|||||||
@@ -13,6 +13,24 @@
|
|||||||
background-color: #f8d7da !important;
|
background-color: #f8d7da !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Last Edited Row Highlighting */
|
||||||
|
.last-edited-row {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
animation: highlight-fade 5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes highlight-fade {
|
||||||
|
0% {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Modal Layout
|
* Modal Layout
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
// Track last edited article for highlighting
|
||||||
|
window.TT_CONFIG.lastEditedArticleId = null;
|
||||||
|
|
||||||
window.TT_CONFIG.CRUD_CONFIG.customRowClass = (row) => {
|
window.TT_CONFIG.CRUD_CONFIG.customRowClass = (row) => {
|
||||||
if (row.isEndOfLife) return 'end-of-life';
|
const classes = [];
|
||||||
|
if (row.isEndOfLife) classes.push('end-of-life');
|
||||||
|
if (window.TT_CONFIG.lastEditedArticleId && row.id == window.TT_CONFIG.lastEditedArticleId) {
|
||||||
|
classes.push('last-edited-row');
|
||||||
|
}
|
||||||
|
return classes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApiResponse(responsePromise) {
|
async function handleApiResponse(responsePromise) {
|
||||||
@@ -14,15 +22,19 @@ async function handleApiResponse(responsePromise) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('warehouse-article-prices', {
|
Vue.component('warehouse-article-prices', {
|
||||||
props: {id: {type: Number, required: true}},
|
props: {
|
||||||
|
id: {type: Number, required: true},
|
||||||
|
cheapestPurchasePrice: {type: Number, default: null}
|
||||||
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="wa-prices-section">
|
<div class="wa-prices-section">
|
||||||
<h5 class="wa-section-title"><i class="fas fa-tags mr-2"></i>Artikelpreise überschreiben</h5>
|
<h5 class="wa-section-title"><i class="fas fa-tags mr-2"></i>Artikelpreise überschreiben</h5>
|
||||||
<div class="wa-prices-grid-dense">
|
<div class="wa-prices-grid-dense">
|
||||||
<div v-for="(price, typeTitle) in articlePrices" :key="typeTitle" class="wa-price-item">
|
<div v-for="price in sortedPrices" :key="price.typeTitle" class="wa-price-item">
|
||||||
<div class="wa-price-header">
|
<div class="wa-price-header">
|
||||||
<i v-if="price.isRobot" class="fas fa-robot mr-1 text-muted" style="font-size: 0.75rem;"></i>
|
<i v-if="price.isRobot" class="fas fa-robot mr-1 text-muted" style="font-size: 0.75rem;"></i>
|
||||||
<strong style="font-size: 0.8rem;">{{ typeTitle }}</strong>
|
<strong style="font-size: 0.8rem;">{{ price.typeTitle }}</strong>
|
||||||
|
<span class="ml-1 text-muted" style="font-size: 0.75rem;">( {{ formatPrice(calculateCurrentPrice(price)) }} )</span>
|
||||||
<i v-if="price.pendingChanges" class="fas fa-exclamation-triangle text-warning ml-auto"
|
<i v-if="price.pendingChanges" class="fas fa-exclamation-triangle text-warning ml-auto"
|
||||||
style="font-size: 0.75rem;" title="Nicht gespeichert"></i>
|
style="font-size: 0.75rem;" title="Nicht gespeichert"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,9 +52,6 @@ Vue.component('warehouse-article-prices', {
|
|||||||
type="number"
|
type="number"
|
||||||
sm no-form-group/>
|
sm no-form-group/>
|
||||||
<div class="wa-price-actions">
|
<div class="wa-price-actions">
|
||||||
<button class="btn btn-sm btn-primary" @click="savePrice(price)" title="Speichern">
|
|
||||||
<i class="fas fa-save"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-danger" @click="deletePrice(price)" :disabled="price.isRobot" title="Löschen">
|
<button class="btn btn-sm btn-danger" @click="deletePrice(price)" :disabled="price.isRobot" title="Löschen">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -52,11 +61,51 @@ Vue.component('warehouse-article-prices', {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data: () => ({window, articlePrices: {}}),
|
data: () => ({window, articlePrices: {}, priceTypes: []}),
|
||||||
|
computed: {
|
||||||
|
sortedPrices() {
|
||||||
|
// Sort: Verkauf first, Partner second, rest alphabetically
|
||||||
|
const priceOrder = {'Verkauf': 1, 'Partner': 2, 'Energie Steiermark': 3};
|
||||||
|
return Object.entries(this.articlePrices)
|
||||||
|
.map(([typeTitle, price]) => ({...price, typeTitle}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const orderA = priceOrder[a.typeTitle] || 99;
|
||||||
|
const orderB = priceOrder[b.typeTitle] || 99;
|
||||||
|
if (orderA !== orderB) return orderA - orderB;
|
||||||
|
return a.typeTitle.localeCompare(b.typeTitle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.fetchArticlePrices();
|
await this.fetchArticlePrices();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatPrice(price) {
|
||||||
|
if (price === null || price === undefined || isNaN(price)) return '-- €';
|
||||||
|
return price.toFixed(2).replace('.', ',') + ' €';
|
||||||
|
},
|
||||||
|
calculateCurrentPrice(price) {
|
||||||
|
const basePrice = this.cheapestPurchasePrice;
|
||||||
|
if (basePrice === null || basePrice === undefined) return null;
|
||||||
|
|
||||||
|
// If custom override price is set, use it
|
||||||
|
if (price.priceOverride !== null && price.priceOverride !== undefined && price.priceOverride !== '') {
|
||||||
|
return parseFloat(price.priceOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If custom multiplier is set, use it
|
||||||
|
if (price.priceMultiplier !== null && price.priceMultiplier !== undefined && price.priceMultiplier !== '') {
|
||||||
|
return basePrice * parseFloat(price.priceMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default factor from price type
|
||||||
|
const priceType = this.priceTypes.find(pt => pt.id === price.articlePriceTypeId);
|
||||||
|
if (priceType && priceType.defaultPriceFactor) {
|
||||||
|
return basePrice * priceType.defaultPriceFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
handleFactorInput(price) {
|
handleFactorInput(price) {
|
||||||
if (price.priceMultiplier) price.priceOverride = null;
|
if (price.priceMultiplier) price.priceOverride = null;
|
||||||
price.pendingChanges = true;
|
price.pendingChanges = true;
|
||||||
@@ -70,15 +119,16 @@ Vue.component('warehouse-article-prices', {
|
|||||||
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/get`, {filters: {articleId: this.id}}),
|
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/get`, {filters: {articleId: this.id}}),
|
||||||
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePriceType/get`)
|
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePriceType/get`)
|
||||||
]);
|
]);
|
||||||
|
this.priceTypes = typesRes.data.rows || [];
|
||||||
const prices = {};
|
const prices = {};
|
||||||
typesRes.data.rows.forEach(type => prices[type.title] = {
|
this.priceTypes.forEach(type => prices[type.title] = {
|
||||||
isRobot: true,
|
isRobot: true,
|
||||||
articlePriceTypeId: type.id,
|
articlePriceTypeId: type.id,
|
||||||
priceMultiplier: type.defaultPriceFactor,
|
priceMultiplier: type.defaultPriceFactor,
|
||||||
priceOverride: null
|
priceOverride: null
|
||||||
});
|
});
|
||||||
pricesRes.data.rows.forEach(pData => {
|
pricesRes.data.rows.forEach(pData => {
|
||||||
const type = typesRes.data.rows.find(t => t.id === pData.articlePriceTypeId);
|
const type = this.priceTypes.find(t => t.id === pData.articlePriceTypeId);
|
||||||
if (!type) return;
|
if (!type) return;
|
||||||
prices[type.title] = {
|
prices[type.title] = {
|
||||||
id: pData.id,
|
id: pData.id,
|
||||||
@@ -91,18 +141,25 @@ Vue.component('warehouse-article-prices', {
|
|||||||
});
|
});
|
||||||
this.articlePrices = prices;
|
this.articlePrices = prices;
|
||||||
},
|
},
|
||||||
async savePrice(price) {
|
async savePrices() {
|
||||||
const payload = {
|
// Save all prices with pending changes
|
||||||
articleId: this.id,
|
const pendingPrices = this.sortedPrices.filter(p => p.pendingChanges);
|
||||||
articlePriceTypeId: price.articlePriceTypeId,
|
for (const price of pendingPrices) {
|
||||||
priceMultiplier: price.priceMultiplier ? parseFloat(price.priceMultiplier.toString().replace(',', '.')) : null,
|
const payload = {
|
||||||
priceOverride: price.priceOverride ? parseFloat(price.priceOverride.toString().replace(',', '.')) : null
|
articleId: this.id,
|
||||||
};
|
articlePriceTypeId: price.articlePriceTypeId,
|
||||||
const endpoint = price.isRobot ? 'create' : 'update';
|
priceMultiplier: price.priceMultiplier ? parseFloat(price.priceMultiplier.toString().replace(',', '.')) : null,
|
||||||
const data = price.isRobot ? payload : {id: price.id, ...payload};
|
priceOverride: price.priceOverride ? parseFloat(price.priceOverride.toString().replace(',', '.')) : null
|
||||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${endpoint}`, data));
|
};
|
||||||
|
const endpoint = price.isRobot ? 'create' : 'update';
|
||||||
|
const data = price.isRobot ? payload : {id: price.id, ...payload};
|
||||||
|
await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${endpoint}`, data);
|
||||||
|
}
|
||||||
await this.fetchArticlePrices();
|
await this.fetchArticlePrices();
|
||||||
},
|
},
|
||||||
|
hasPendingChanges() {
|
||||||
|
return this.sortedPrices.some(p => p.pendingChanges);
|
||||||
|
},
|
||||||
async deletePrice(price) {
|
async deletePrice(price) {
|
||||||
const payload = {id: price.id, articleId: this.id, articlePriceTypeId: price.articlePriceTypeId}
|
const payload = {id: price.id, articleId: this.id, articlePriceTypeId: price.articlePriceTypeId}
|
||||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/delete`, payload));
|
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/delete`, payload));
|
||||||
@@ -243,9 +300,6 @@ Vue.component('warehouse-article-distributor', {
|
|||||||
sm no-form-group/>
|
sm no-form-group/>
|
||||||
</div>
|
</div>
|
||||||
<div class="wa-distributor-actions">
|
<div class="wa-distributor-actions">
|
||||||
<button class="btn btn-sm btn-primary" @click="saveDistributor(distributor)" title="Speichern">
|
|
||||||
<i class="fas fa-save"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-danger" @click="deleteDistributor(distributor.id)" :disabled="!distributor.id" title="Löschen">
|
<button class="btn btn-sm btn-danger" @click="deleteDistributor(distributor.id)" :disabled="!distributor.id" title="Löschen">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -292,10 +346,18 @@ Vue.component('warehouse-article-distributor', {
|
|||||||
pendingChanges: true
|
pendingChanges: true
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async saveDistributor(distributor) {
|
hasPendingChanges() {
|
||||||
delete distributor.pendingChanges;
|
return this.articleDistributors.some(d => d.pendingChanges || !d.id);
|
||||||
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));
|
async saveDistributors() {
|
||||||
|
// Save all distributors with pending changes or newly added ones
|
||||||
|
const pendingDistributors = this.articleDistributors.filter(d => d.pendingChanges || !d.id);
|
||||||
|
for (const distributor of pendingDistributors) {
|
||||||
|
const data = {...distributor};
|
||||||
|
delete data.pendingChanges;
|
||||||
|
data.purchasePrice = data.purchasePrice ? parseFloat(data.purchasePrice.toString().replace(',', '.')) : null;
|
||||||
|
await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${data.id ? 'update' : 'create'}`, data);
|
||||||
|
}
|
||||||
await this.fetchArticleDistributors();
|
await this.fetchArticleDistributors();
|
||||||
},
|
},
|
||||||
async deleteDistributor(distributorId) {
|
async deleteDistributor(distributorId) {
|
||||||
@@ -385,10 +447,10 @@ Vue.component('warehouse-article-modal', {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prices Section -->
|
<!-- Prices Section -->
|
||||||
<warehouse-article-prices v-if="isEditMode" :id="Number(id)"/>
|
<warehouse-article-prices v-if="isEditMode" ref="pricesComponent" :id="Number(id)" :cheapest-purchase-price="cheapestPurchasePrice"/>
|
||||||
|
|
||||||
<!-- Distributors Section -->
|
<!-- Distributors Section -->
|
||||||
<warehouse-article-distributor v-if="isEditMode" :id="Number(id)"/>
|
<warehouse-article-distributor v-if="isEditMode" ref="distributorComponent" :id="Number(id)"/>
|
||||||
|
|
||||||
<!-- Additional Attributes -->
|
<!-- Additional Attributes -->
|
||||||
<div class="wa-section" :class="{ 'wa-field-disabled': !isEditMode && !formData.category_id }">
|
<div class="wa-section" :class="{ 'wa-field-disabled': !isEditMode && !formData.category_id }">
|
||||||
@@ -450,11 +512,16 @@ Vue.component('warehouse-article-modal', {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" @click="close">Abbrechen</button>
|
<button type="button" class="btn btn-secondary" @click="close">Abbrechen</button>
|
||||||
<button type="button" class="btn btn-primary" @click="save" :disabled="saving || !isValid">
|
<button type="button" class="btn btn-outline-primary" @click="save(false)" :disabled="saving || !isValid">
|
||||||
<i v-if="saving" class="fas fa-spinner fa-spin mr-1"></i>
|
<i v-if="saving" class="fas fa-spinner fa-spin mr-1"></i>
|
||||||
<i v-else class="fas fa-save mr-1"></i>
|
<i v-else class="fas fa-save mr-1"></i>
|
||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="save(true)" :disabled="saving || !isValid">
|
||||||
|
<i v-if="saving" class="fas fa-spinner fa-spin mr-1"></i>
|
||||||
|
<i v-else class="fas fa-save mr-1"></i>
|
||||||
|
Speichern und Schließen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -463,6 +530,9 @@ Vue.component('warehouse-article-modal', {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
|
originalCategoryId: null,
|
||||||
|
cheapestPurchasePrice: null,
|
||||||
|
originalFormData: null,
|
||||||
formData: {
|
formData: {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -548,6 +618,12 @@ Vue.component('warehouse-article-modal', {
|
|||||||
isSbidiShop: !!data.isSbidiShop,
|
isSbidiShop: !!data.isSbidiShop,
|
||||||
isSbidiShopHide: !!data.isSbidiShopHide
|
isSbidiShopHide: !!data.isSbidiShopHide
|
||||||
};
|
};
|
||||||
|
// Store original category to detect changes
|
||||||
|
this.originalCategoryId = data.category_id;
|
||||||
|
// Store cheapest purchase price for price calculations
|
||||||
|
this.cheapestPurchasePrice = data.cheapestPurchasePrice || null;
|
||||||
|
// Store original form data to detect changes
|
||||||
|
this.originalFormData = JSON.stringify(this.formData);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.notify('error', 'Fehler beim Laden');
|
window.notify('error', 'Fehler beim Laden');
|
||||||
@@ -574,7 +650,10 @@ Vue.component('warehouse-article-modal', {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async onCategoryChange(categoryId) {
|
async onCategoryChange(categoryId) {
|
||||||
if (!categoryId || this.isEditMode) return;
|
if (!categoryId) return;
|
||||||
|
// In edit mode, only regenerate if category actually changed from original
|
||||||
|
if (this.isEditMode && categoryId == this.originalCategoryId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/getNextArticleNumber`, {
|
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/getNextArticleNumber`, {
|
||||||
params: { categoryId: categoryId }
|
params: { categoryId: categoryId }
|
||||||
@@ -586,29 +665,77 @@ Vue.component('warehouse-article-modal', {
|
|||||||
console.error('Failed to get next article number:', e);
|
console.error('Failed to get next article number:', e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async save() {
|
async save(closeAfterSave = true) {
|
||||||
if (!this.isValid) return;
|
if (!this.isValid) return;
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
try {
|
try {
|
||||||
const endpoint = this.isEditMode ? 'update' : 'create';
|
let savedPrices = false;
|
||||||
const payload = {
|
let savedDistributors = false;
|
||||||
...this.formData,
|
let savedArticle = false;
|
||||||
isSerialDocumentation: this.formData.isSerialDocumentation ? 1 : 0,
|
|
||||||
isEndOfLife: this.formData.isEndOfLife ? 1 : 0,
|
|
||||||
isEShop: this.formData.isEShop ? 1 : 0,
|
|
||||||
isEShopHide: this.formData.isEShopHide ? 1 : 0,
|
|
||||||
isSbidiShop: this.formData.isSbidiShop ? 1 : 0,
|
|
||||||
isSbidiShopHide: this.formData.isSbidiShopHide ? 1 : 0
|
|
||||||
};
|
|
||||||
if (this.isEditMode) payload.id = Number(this.id);
|
|
||||||
|
|
||||||
const res = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/${endpoint}`, payload);
|
// Save prices and distributors first (only in edit mode)
|
||||||
if (res.data.success) {
|
if (this.isEditMode) {
|
||||||
window.notify('success', res.data.message || 'Gespeichert');
|
if (this.$refs.pricesComponent && this.$refs.pricesComponent.hasPendingChanges()) {
|
||||||
if (!this.isEditMode && window.TT_CONFIG.CRUD_CONFIG.reopenOnCreate) this.$emit('reopen', res.data.id);
|
await this.$refs.pricesComponent.savePrices();
|
||||||
else this.$emit('close');
|
savedPrices = true;
|
||||||
|
}
|
||||||
|
if (this.$refs.distributorComponent && this.$refs.distributorComponent.hasPendingChanges()) {
|
||||||
|
await this.$refs.distributorComponent.saveDistributors();
|
||||||
|
savedDistributors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if main article data actually changed
|
||||||
|
const currentFormData = JSON.stringify(this.formData);
|
||||||
|
const articleDataChanged = !this.isEditMode || this.originalFormData !== currentFormData;
|
||||||
|
|
||||||
|
if (articleDataChanged) {
|
||||||
|
const endpoint = this.isEditMode ? 'update' : 'create';
|
||||||
|
const payload = {
|
||||||
|
...this.formData,
|
||||||
|
isSerialDocumentation: this.formData.isSerialDocumentation ? 1 : 0,
|
||||||
|
isEndOfLife: this.formData.isEndOfLife ? 1 : 0,
|
||||||
|
isEShop: this.formData.isEShop ? 1 : 0,
|
||||||
|
isEShopHide: this.formData.isEShopHide ? 1 : 0,
|
||||||
|
isSbidiShop: this.formData.isSbidiShop ? 1 : 0,
|
||||||
|
isSbidiShopHide: this.formData.isSbidiShopHide ? 1 : 0
|
||||||
|
};
|
||||||
|
if (this.isEditMode) payload.id = Number(this.id);
|
||||||
|
|
||||||
|
const res = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/${endpoint}`, payload);
|
||||||
|
if (res.data.success) {
|
||||||
|
savedArticle = true;
|
||||||
|
// Track last edited article for row highlighting
|
||||||
|
window.TT_CONFIG.lastEditedArticleId = this.isEditMode ? Number(this.id) : res.data.id;
|
||||||
|
|
||||||
|
if (!this.isEditMode) {
|
||||||
|
// For new articles, reopen in edit mode
|
||||||
|
window.notify('success', res.data.message || 'Gespeichert');
|
||||||
|
this.$emit('reopen', res.data.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.notify('error', res.data.message || 'Fehler beim Speichern');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message if anything was saved
|
||||||
|
if (savedArticle || savedPrices || savedDistributors) {
|
||||||
|
window.TT_CONFIG.lastEditedArticleId = Number(this.id);
|
||||||
|
window.notify('success', 'Gespeichert');
|
||||||
} else {
|
} else {
|
||||||
window.notify('error', res.data.message || 'Fehler beim Speichern');
|
window.notify('info', 'Keine Änderungen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeAfterSave) {
|
||||||
|
// Close modal if requested
|
||||||
|
this.$emit('close');
|
||||||
|
} else {
|
||||||
|
// Stay open - reload data to refresh prices/distributors
|
||||||
|
await this.loadArticle();
|
||||||
|
if (this.$refs.pricesComponent) await this.$refs.pricesComponent.fetchArticlePrices();
|
||||||
|
if (this.$refs.distributorComponent) await this.$refs.distributorComponent.fetchArticleDistributors();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.notify('error', 'Fehler beim Speichern');
|
window.notify('error', 'Fehler beim Speichern');
|
||||||
|
|||||||
Reference in New Issue
Block a user