// Track last edited article for highlighting window.TT_CONFIG.lastEditedArticleId = null; window.TT_CONFIG.CRUD_CONFIG.customRowClass = (row) => { 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) { const res = await responsePromise; if (!res.data.success) { const errors = res.data.errors; const errorMessage = Array.isArray(errors) ? errors.join(', ') : Object.values(errors).join(', '); return window.notify('error', `Fehler: ${errorMessage}`); } window.notify('success', res.data.message || 'Erfolgreich'); window.dispatchEvent(new Event('refreshTable')); } Vue.component('warehouse-article-prices', { props: { id: {type: Number, required: true}, cheapestPurchasePrice: {type: Number, default: null} }, template: `
Artikelpreise überschreiben
`, 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() { await this.fetchArticlePrices(); }, 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) { if (price.priceMultiplier) price.priceOverride = null; price.pendingChanges = true; }, handlePriceInput(price) { if (price.priceOverride) price.priceMultiplier = null; price.pendingChanges = true; }, async fetchArticlePrices() { const [pricesRes, typesRes] = await Promise.all([ axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/get`, {filters: {articleId: this.id}}), axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePriceType/get`) ]); this.priceTypes = typesRes.data.rows || []; const prices = {}; this.priceTypes.forEach(type => prices[type.title] = { isRobot: true, articlePriceTypeId: type.id, priceMultiplier: type.defaultPriceFactor, priceOverride: null }); pricesRes.data.rows.forEach(pData => { const type = this.priceTypes.find(t => t.id === pData.articlePriceTypeId); if (!type) return; prices[type.title] = { id: pData.id, isRobot: false, pendingChanges: false, articlePriceTypeId: pData.articlePriceTypeId, priceMultiplier: pData.priceMultiplier, priceOverride: pData.priceOverride }; }); this.articlePrices = prices; }, async savePrices() { // Save all prices with pending changes const pendingPrices = this.sortedPrices.filter(p => p.pendingChanges); for (const price of pendingPrices) { const payload = { articleId: this.id, articlePriceTypeId: price.articlePriceTypeId, priceMultiplier: price.priceMultiplier ? parseFloat(price.priceMultiplier.toString().replace(',', '.')) : null, priceOverride: price.priceOverride ? parseFloat(price.priceOverride.toString().replace(',', '.')) : null }; 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(); }, hasPendingChanges() { return this.sortedPrices.some(p => p.pendingChanges); }, async deletePrice(price) { 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.fetchArticlePrices(); } } }); Vue.component('warehouse-distributor-directory-modal', { props: { show: { type: Boolean, default: false }, allDistributors: { type: Array, default: () => [] }, articleDistributors: { type: Array, default: () => [] } }, data: () => ({ distributorSearch: '' }), watch: { show(newVal) { if (newVal) document.documentElement.style.overflow = 'hidden'; } }, computed: { filteredDistributors() { if (!this.distributorSearch) return this.allDistributors; const search = this.distributorSearch.toLowerCase(); return this.allDistributors.filter(d => d.name.toLowerCase().includes(search)); }, alphabetWithDistributors() { const letters = new Set(); this.filteredDistributors.forEach(d => { const firstChar = d.name.charAt(0).toUpperCase(); if (/[A-Z]/.test(firstChar)) letters.add(firstChar); }); return Array.from(letters).sort(); } }, methods: { getDistributorsByLetter(letter) { return this.filteredDistributors.filter(d => d.name.charAt(0).toUpperCase() === letter); }, isDistributorAdded(distributorId) { return this.articleDistributors.some(d => d.distributorId === distributorId); }, selectDistributor(distributorId) { this.$emit('select', distributorId); this.$emit('close'); }, close() { this.$emit('close'); } }, template: ` ` }); Vue.component('warehouse-article-distributor', { props: {id: {type: Number, required: true}}, template: `
Keine Lieferanten zugewiesen
`, data: () => ({ window, articleDistributors: [], allDistributors: [], showDirectoryModal: false }), async mounted() { await Promise.all([ this.fetchArticleDistributors(), this.fetchAllDistributors() ]); }, methods: { async fetchAllDistributors() { const res = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseDistributor/get`, { pagination: { per_page: 10000 }, order: { key: 'name', order: 'ASC' } }); this.allDistributors = res.data.rows || []; }, async fetchArticleDistributors() { const res = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/get`, {filters: {articleId: this.id}}); this.articleDistributors = res.data.rows; }, getDistributorName(id) { const dist = this.allDistributors.find(d => d.id === id); return dist ? dist.name : 'Unbekannt'; }, addDistributor(distributorId) { if (this.articleDistributors.some(d => d.distributorId === distributorId)) return; this.articleDistributors.push({ articleId: this.id, distributorId: distributorId, externalArticleNumber: null, purchasePrice: null, pendingChanges: true }); }, hasPendingChanges() { return this.articleDistributors.some(d => d.pendingChanges || !d.id); }, 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(); }, async deleteDistributor(distributorId) { if (!confirm('Lieferant wirklich entfernen?')) return; await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/delete`, {id: distributorId, articleId: this.id})); await this.fetchArticleDistributors(); } } }); Vue.component('warehouse-article-modal', { props: { id: { type: [Number, String], required: true } }, template: ` `, data: () => ({ loading: false, saving: false, originalCategoryId: null, cheapestPurchasePrice: null, originalFormData: null, formData: { title: '', description: '', category_id: null, articleNumber: '', unit: 'Stk.', vatgroup_id: 2, warningAmount: 0, criticalAmount: 0, isSerialDocumentation: false, isEndOfLife: false, isEShop: false, isEShopHide: false, isSbidiShop: false, isSbidiShopHide: false } }), computed: { isEditMode() { return this.id !== 'create'; }, categoryOptions() { const catCol = window.TT_CONFIG.CRUD_CONFIG.columns.find(c => c.key === 'category_id'); return catCol ? [{ value: null, text: '-- Bitte wählen --' }, ...catCol.modal.items] : []; }, unitOptions() { return [ { value: 'Stk.', text: 'Stk.' }, { value: 'Pau.', text: 'Pau.' }, { value: 'm.', text: 'm.' }, { value: 'Std.', text: 'Std.' }, { value: 'km', text: 'km' } ]; }, vatgroupOptions() { return [ { value: 2, text: 'Dienstleistungen' }, { value: 3, text: 'Handelswaren' } ]; }, isValid() { return this.formData.title && this.formData.description && this.formData.category_id && this.formData.articleNumber && this.formData.unit; } }, mounted() { const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; document.documentElement.style.overflow = 'hidden'; document.documentElement.style.paddingRight = scrollbarWidth + 'px'; if (this.isEditMode) this.loadArticle(); else this.resetForm(); }, beforeDestroy() { document.documentElement.style.overflow = ''; document.documentElement.style.paddingRight = ''; }, methods: { async loadArticle() { this.loading = true; try { const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/getById`, { params: { id: Number(this.id) } }); const data = res.data; if (data && data.id) { this.formData = { title: data.title || '', description: data.description || '', category_id: data.category_id, articleNumber: data.articleNumber || '', unit: data.unit || 'Stk.', vatgroup_id: data.vatgroup_id || 2, warningAmount: data.warningAmount || 0, criticalAmount: data.criticalAmount || 0, isSerialDocumentation: !!data.isSerialDocumentation, isEndOfLife: !!data.isEndOfLife, isEShop: !!data.isEShop, isEShopHide: !!data.isEShopHide, isSbidiShop: !!data.isSbidiShop, 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) { window.notify('error', 'Fehler beim Laden'); } finally { this.loading = false; } }, resetForm() { this.formData = { title: '', description: '', category_id: null, articleNumber: '', unit: 'Stk.', vatgroup_id: 2, warningAmount: 0, criticalAmount: 0, isSerialDocumentation: false, isEndOfLife: false, isEShop: false, isEShopHide: false, isSbidiShop: false, isSbidiShopHide: false }; }, async onCategoryChange(categoryId) { if (!categoryId) return; // In edit mode, only regenerate if category actually changed from original if (this.isEditMode && categoryId == this.originalCategoryId) return; try { const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/getNextArticleNumber`, { params: { categoryId: categoryId } }); if (res.data.success) { this.formData.articleNumber = res.data.articleNumber; } } catch (e) { console.error('Failed to get next article number:', e); } }, async save(closeAfterSave = true) { if (!this.isValid) return; this.saving = true; try { let savedPrices = false; let savedDistributors = false; let savedArticle = false; // Save prices and distributors first (only in edit mode) if (this.isEditMode) { if (this.$refs.pricesComponent && this.$refs.pricesComponent.hasPendingChanges()) { await this.$refs.pricesComponent.savePrices(); 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 { 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) { window.notify('error', 'Fehler beim Speichern'); } finally { this.saving = false; } }, close() { this.$emit('close'); } } }); Vue.component('warehouse-article', { template: `
`, data: () => ({ window, historyModal: false, historyModalId: null, articleModalId: null }), mounted() { const table = this.$refs.table?.$refs?.table; if (!table) return; const showId = new URLSearchParams(window.location.search).get('showId'); if (showId && (!table.filters || table.filters.id !== showId)) { table.filters = {...table.filters, id: showId}; table.refreshTable(); } else if (!showId && table.filters?.id) { delete table.filters.id; if (Object.keys(table.filters).length === 0) table.filters = {}; table.refreshTable(); } }, methods: { printLabel(event) { const url = window.TT_CONFIG.BASE_PATH + "/WarehouseArticle/printLabel?id=" + event.id; window.open(url, '_blank'); } } });