/** * Produktverwaltung - JavaScript * Baum auf-/zuklappen, Inline-Editing, AJAX-Aktionen */ document.addEventListener('DOMContentLoaded', function() { 'use strict'; // Only init on produktverwaltung pages if (!document.querySelector('.pv-tree, .pv-no-category')) return; var pvAjaxUrl = pvConfig ? pvConfig.ajaxUrl : ''; var pvToken = pvConfig ? pvConfig.token : ''; var pvHasMargin = pvConfig ? pvConfig.hasMargin : false; // === Toast Notification === function showToast(message, type) { var toast = document.createElement('div'); toast.className = 'pv-toast ' + (type || 'success'); toast.textContent = message; document.body.appendChild(toast); setTimeout(function() { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(function() { toast.remove(); }, 300); }, 2500); } // === Baum auf-/zuklappen === function toggleCategory(header) { var toggle = header.querySelector('.pv-toggle'); var children = header.nextElementSibling; if (!children) return; if (children.classList.contains('collapsed')) { children.classList.remove('collapsed'); if (toggle) toggle.classList.remove('collapsed'); } else { children.classList.add('collapsed'); if (toggle) toggle.classList.add('collapsed'); } } // Alle aufklappen window.pvExpandAll = function() { document.querySelectorAll('.pv-category-children.collapsed').forEach(function(el) { el.classList.remove('collapsed'); }); document.querySelectorAll('.pv-toggle.collapsed').forEach(function(el) { el.classList.remove('collapsed'); }); // Also expand no-category section var ncBody = document.querySelector('.pv-no-category-body'); if (ncBody) ncBody.classList.remove('collapsed'); }; // Alle zuklappen window.pvCollapseAll = function() { document.querySelectorAll('.pv-category-children').forEach(function(el) { el.classList.add('collapsed'); }); document.querySelectorAll('.pv-toggle').forEach(function(el) { el.classList.add('collapsed'); }); }; // Category header click document.querySelectorAll('.pv-category-header').forEach(function(header) { header.addEventListener('click', function(e) { if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') return; toggleCategory(header); }); }); // No-category header toggle var ncHeader = document.querySelector('.pv-no-category-header'); if (ncHeader) { ncHeader.addEventListener('click', function() { var body = document.querySelector('.pv-no-category-body'); if (body) body.classList.toggle('collapsed'); }); } // Archiv header toggle var archiveHeader = document.querySelector('.pv-archive-header'); if (archiveHeader) { archiveHeader.addEventListener('click', function() { var body = document.querySelector('.pv-archive-body'); if (body) body.classList.toggle('collapsed'); }); } // === Ref-Schema toggle === var schemaBox = document.querySelector('.pv-ref-schema'); if (schemaBox) { var schemaHeader = schemaBox.querySelector('.pv-schema-header'); if (schemaHeader) { schemaHeader.addEventListener('click', function() { schemaBox.classList.toggle('collapsed'); }); } } // === Suche / Filter === var searchInput = document.querySelector('.pv-search input'); if (searchInput) { var searchTimeout; searchInput.addEventListener('input', function() { clearTimeout(searchTimeout); searchTimeout = setTimeout(function() { filterTree(searchInput.value.toLowerCase().trim()); }, 250); }); } function filterTree(query) { if (!query) { // Show all document.querySelectorAll('.pv-tree li, .pv-products tr').forEach(function(el) { el.style.display = ''; }); document.querySelectorAll('.pv-category-children').forEach(function(el) { el.classList.add('collapsed'); }); document.querySelectorAll('.pv-toggle').forEach(function(el) { el.classList.add('collapsed'); }); return; } // Search in product rows var anyMatch = false; document.querySelectorAll('.pv-products tbody tr').forEach(function(row) { var ref = (row.querySelector('.pv-col-ref') || {}).textContent || ''; var label = (row.querySelector('.pv-col-label') || {}).textContent || ''; var match = ref.toLowerCase().indexOf(query) >= 0 || label.toLowerCase().indexOf(query) >= 0; row.style.display = match ? '' : 'none'; if (match) anyMatch = true; }); // Expand categories that have visible products document.querySelectorAll('.pv-tree li').forEach(function(li) { var table = li.querySelector('.pv-products'); var hasVisible = false; if (table) { table.querySelectorAll('tbody tr').forEach(function(row) { if (row.style.display !== 'none') hasVisible = true; }); } // Check child categories too var childLis = li.querySelectorAll(':scope > .pv-category-children > li'); childLis.forEach(function(child) { if (child.style.display !== 'none') hasVisible = true; }); if (hasVisible) { li.style.display = ''; var children = li.querySelector('.pv-category-children'); if (children) children.classList.remove('collapsed'); var toggle = li.querySelector('.pv-toggle'); if (toggle) toggle.classList.remove('collapsed'); } else { // Check if category name matches var catLabel = li.querySelector('.pv-cat-label'); if (catLabel && catLabel.textContent.toLowerCase().indexOf(query) >= 0) { li.style.display = ''; hasVisible = true; } else { li.style.display = hasVisible ? '' : 'none'; } } }); // Also search in no-category section var ncBody = document.querySelector('.pv-no-category-body'); if (ncBody) { var ncMatch = false; ncBody.querySelectorAll('tbody tr').forEach(function(row) { var ref = (row.querySelector('.pv-col-ref') || {}).textContent || ''; var label = (row.querySelector('.pv-col-label') || {}).textContent || ''; var match = ref.toLowerCase().indexOf(query) >= 0 || label.toLowerCase().indexOf(query) >= 0; row.style.display = match ? '' : 'none'; if (match) ncMatch = true; }); if (ncMatch) ncBody.classList.remove('collapsed'); } } // === Inline Editing === document.querySelectorAll('.pv-editable').forEach(function(el) { el.addEventListener('dblclick', function(e) { e.stopPropagation(); startEditing(el); }); }); function startEditing(el) { if (el.querySelector('.pv-edit-input')) return; // Already editing var field = el.dataset.field; // 'ref', 'label', 'description', or 'margin' var productId = el.dataset.productId; var currentValue = el.textContent.trim(); // Margin-Feld: nur den Zahlenwert extrahieren (ohne %, ohne Warn-Icons) if (field === 'margin') { currentValue = currentValue.replace(/[^0-9,.\-]/g, '').trim(); } var input = document.createElement('input'); input.type = 'text'; input.className = 'pv-edit-input'; input.value = currentValue; if (field === 'ref') { input.style.textTransform = 'uppercase'; } if (field === 'margin') { input.style.width = '70px'; input.style.textAlign = 'right'; input.placeholder = '%'; } el.textContent = ''; el.appendChild(input); input.focus(); input.select(); function save() { var newValue = input.value.trim(); if (field === 'ref') { newValue = newValue.toUpperCase(); } if (newValue === currentValue || newValue === '') { cancel(); return; } // Margin-Feld: preisbot_margin Extrafield speichern if (field === 'margin') { var parsedVal = newValue.replace(',', '.'); el.classList.add('pv-saving'); ajaxCall('update_margin', { product_id: productId, value: parsedVal }, function(response) { el.classList.remove('pv-saving'); if (response.success) { el.textContent = newValue.replace('.', ',') + '%'; showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); } else { el.textContent = currentValue ? currentValue + '%' : '-'; showToast(response.error || pvLang.saveError || 'Fehler', 'error'); } }, function() { el.classList.remove('pv-saving'); el.textContent = currentValue ? currentValue + '%' : '-'; showToast(pvLang.saveError || 'Fehler beim Speichern', 'error'); }); return; } el.classList.add('pv-saving'); ajaxCall('update_' + field, { product_id: productId, value: newValue }, function(response) { el.classList.remove('pv-saving'); if (response.success) { el.textContent = newValue; showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); } else { el.textContent = currentValue; showToast(response.error || pvLang.saveError || 'Fehler', 'error'); } }, function() { el.classList.remove('pv-saving'); el.textContent = currentValue; showToast(pvLang.saveError || 'Fehler beim Speichern', 'error'); }); } function cancel() { if (field === 'margin') { el.textContent = currentValue ? currentValue + '%' : '-'; } else { el.textContent = currentValue; } } input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); save(); } else if (e.key === 'Escape') { cancel(); } }); input.addEventListener('blur', function() { // Small delay to allow click events setTimeout(function() { if (el.contains(input)) { save(); } }, 100); }); } // === Produkt löschen === window.pvDeleteProduct = function(productId, productRef) { var msg = (pvLang.confirmDelete || 'Produkt "%s" wirklich löschen?').replace('%s', productRef); if (!confirm(msg)) return; ajaxCall('delete_product', { product_id: productId }, function(response) { if (response.success) { // Remove row from table var row = document.querySelector('tr[data-product-id="' + productId + '"]'); if (row) row.remove(); showToast(pvLang.productDeleted || 'Produkt gelöscht', 'success'); } else { showToast(response.error || pvLang.deleteError || 'Fehler', 'error'); } }); }; // === Kategorie zuweisen === window.pvAssignCategory = function(productId, productRef) { // Load categories via AJAX ajaxCall('get_categories', {}, function(response) { if (!response.success || !response.categories) { showToast(response.error || 'Fehler beim Laden der Kategorien', 'error'); return; } showCategoryDialog(productId, productRef, response.categories); }, function() { showToast('AJAX-Fehler beim Laden der Kategorien', 'error'); }); }; function showCategoryDialog(productId, productRef, categories) { var overlay = document.createElement('div'); overlay.className = 'pv-dialog-overlay'; var dialog = document.createElement('div'); dialog.className = 'pv-dialog'; dialog.innerHTML = '

' + (pvLang.selectCategory || 'Kategorie zuweisen') + '

' + '

' + productRef + '

' + '' + '
' + '' + '' + '
'; overlay.appendChild(dialog); document.body.appendChild(overlay); // Fill select var select = document.getElementById('pv-cat-select'); categories.forEach(function(cat) { var option = document.createElement('option'); option.value = cat.id; option.textContent = cat.fullpath; select.appendChild(option); }); // Assign button document.getElementById('pv-cat-assign-btn').addEventListener('click', function() { var catId = select.value; if (!catId) return; ajaxCall('add_to_category', { product_id: productId, category_id: catId }, function(response) { overlay.remove(); if (response.success) { showToast(pvLang.categoryAssigned || 'Kategorie zugewiesen', 'success'); // Reload page to reflect changes setTimeout(function() { location.reload(); }, 800); } else { showToast(response.error || 'Fehler', 'error'); } }); }); // Close on overlay click overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); }); } // === Aus Kategorie entfernen === window.pvRemoveFromCategory = function(productId, categoryId) { ajaxCall('remove_from_category', { product_id: productId, category_id: categoryId }, function(response) { if (response.success) { showToast(pvLang.categoryRemoved || 'Aus Kategorie entfernt', 'success'); setTimeout(function() { location.reload(); }, 800); } else { showToast(response.error || 'Fehler', 'error'); } }); }; // === Produkt bearbeiten (Dialog) === window.pvEditProduct = function(productId) { // Fetch current data from server ajaxCall('get_product', { product_id: productId }, function(response) { if (!response.success || !response.product) { showToast(response.error || 'Fehler', 'error'); return; } showEditDialog(response.product); }); }; function showEditDialog(product) { var overlay = document.createElement('div'); overlay.className = 'pv-dialog-overlay'; var doliUrl = (typeof pvDolibarrUrl !== 'undefined' ? pvDolibarrUrl : '') + '/product/card.php?id=' + product.id; var dialog = document.createElement('div'); dialog.className = 'pv-dialog pv-edit-dialog'; dialog.innerHTML = '

' + (pvLang.editProduct || 'Produkt bearbeiten') + '

' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + ' ' + (pvLang.openProductCard || 'Produktkarte') + '' + '' + '' + '' + '
'; overlay.appendChild(dialog); document.body.appendChild(overlay); // Focus ref field var refInput = document.getElementById('pv-edit-ref'); refInput.focus(); refInput.select(); // Save handler var saveBtn = dialog.querySelector('.pv-btn-save'); saveBtn.addEventListener('click', function() { saveProductEdit(product.id, overlay); }); // Cancel handler dialog.querySelector('.pv-btn-cancel').addEventListener('click', function() { overlay.remove(); }); // Close on overlay click overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); }); // Enter to save, Escape to close dialog.addEventListener('keydown', function(e) { if (e.key === 'Enter' && e.target.tagName === 'INPUT') { e.preventDefault(); saveProductEdit(product.id, overlay); } else if (e.key === 'Escape') { overlay.remove(); } }); } function saveProductEdit(productId, overlay) { var saveBtn = overlay.querySelector('.pv-btn-save'); var origText = saveBtn.textContent; saveBtn.textContent = pvLang.saving || 'Speichert...'; saveBtn.disabled = true; var data = { product_id: productId, new_ref: document.getElementById('pv-edit-ref').value.trim(), new_label: document.getElementById('pv-edit-label').value.trim(), new_description: document.getElementById('pv-edit-description').value.trim(), new_sell_price: parsePrice(document.getElementById('pv-edit-sell-price').value) }; ajaxCall('update_product', data, function(response) { if (response.success && response.product) { overlay.remove(); showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); updateProductRow(response.product); } else { saveBtn.textContent = origText; saveBtn.disabled = false; showToast(response.error || pvLang.saveError || 'Fehler', 'error'); } }, function() { saveBtn.textContent = origText; saveBtn.disabled = false; showToast(pvLang.saveError || 'Fehler beim Speichern', 'error'); }); } function updateProductRow(product) { var row = document.querySelector('tr[data-product-id="' + product.id + '"]'); if (!row) return; // Update ref var refEl = row.querySelector('.pv-col-ref .pv-editable'); if (refEl) { refEl.textContent = product.ref; } else { var refTd = row.querySelector('.pv-col-ref'); if (refTd) refTd.textContent = product.ref; } // Update label var labelEl = row.querySelector('.pv-col-label .pv-editable'); if (labelEl) { labelEl.textContent = product.label; } else { var labelTd = row.querySelector('.pv-col-label'); if (labelTd) labelTd.textContent = product.label; } // Update VK price in the row (2nd price cell = VK) var priceCells = row.querySelectorAll('.pv-col-price'); if (priceCells.length >= 2) { priceCells[1].textContent = product.sell_price > 0 ? formatPrice(product.sell_price) : '-'; } // Update data attributes row.setAttribute('data-sell-price', product.sell_price || ''); // Recalculate margin with best buy price from data attribute var bestBuy = parseFloat(row.getAttribute('data-best-buy-price')); var calcMarginEl = row.querySelector('.pv-calc-margin'); if (calcMarginEl && bestBuy > 0 && product.sell_price > 0) { var cm = ((product.sell_price - bestBuy) / bestBuy * 100).toFixed(1); calcMarginEl.textContent = formatPrice(cm) + '%'; } } // Helper: parse price input (German/international format) function parsePrice(val) { if (!val) return ''; val = val.replace(/\s/g, '').replace(/\./g, '').replace(',', '.'); return val; } // Helper: format number for display in input function formatNum(val) { if (!val || val == 0) return ''; return parseFloat(val).toFixed(2).replace('.', ','); } // Helper: format price for table display function formatPrice(val) { if (!val || val == 0) return '-'; return parseFloat(val).toFixed(2).replace('.', ','); } // Helper: escape HTML function escHtml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // === Produkt-Fenster (wiederverwendbar) === var pvProductWindow = null; window.pvOpenProductWindow = function(url) { if (pvProductWindow && !pvProductWindow.closed) { pvProductWindow.location.href = url; pvProductWindow.focus(); } else { pvProductWindow = window.open(url, 'pvProductCard', 'width=1100,height=800,scrollbars=yes,resizable=yes'); } return false; }; // === Kategorie bearbeiten === window.pvEditCategory = function(categoryId) { ajaxCall('get_category_data', { category_id: categoryId }, function(response) { if (!response.success || !response.category) { showToast(response.error || 'Fehler', 'error'); return; } showCategoryEditDialog(response.category, false); }, function() { showToast('Fehler beim Laden der Kategorie', 'error'); }); }; // === Kategorie hinzufügen === window.pvAddCategory = function(parentId) { showCategoryEditDialog({ id: 0, label: '', description: '', color: '', fk_parent: parentId || 0 }, true); }; // === Kategorie löschen === window.pvDeleteCategory = function(categoryId, categoryLabel) { var msg = (pvLang.deleteCategory || 'Kategorie "%s" wirklich löschen?').replace('%s', categoryLabel); if (!confirm(msg)) return; ajaxCall('delete_category', { category_id: categoryId }, function(response) { if (response.success) { showToast(pvLang.categoryDeleted || 'Kategorie gelöscht', 'success'); setTimeout(function() { location.reload(); }, 800); } else { showToast(response.error || 'Fehler', 'error'); } }); }; function showCategoryEditDialog(category, isNew) { var overlay = document.createElement('div'); overlay.className = 'pv-dialog-overlay'; var title = isNew ? (pvLang.addCategory || 'Kategorie hinzufügen') : (pvLang.editCategory || 'Kategorie bearbeiten'); var colorVal = category.color || ''; var dialog = document.createElement('div'); dialog.className = 'pv-dialog pv-edit-dialog'; dialog.innerHTML = '

' + escHtml(title) + '

' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
' + '' + '' + '' + '
'; overlay.appendChild(dialog); document.body.appendChild(overlay); // Init color swatches var presetColors = [ '', 'e74c3c', 'e91e63', '9b59b6', '8e44ad', '3498db', '2980b9', '1abc9c', '16a085', '27ae60', '2ecc71', 'f39c12', 'f1c40f', 'e67e22', 'd35400', '95a5a6', '7f8c8d', '34495e', '2c3e50', '795548' ]; var swatchContainer = document.getElementById('pv-color-swatches'); var hiddenInput = document.getElementById('pv-cat-edit-color'); presetColors.forEach(function(c) { var swatch = document.createElement('span'); swatch.className = 'pv-color-swatch' + (c === colorVal ? ' selected' : ''); swatch.style.background = c ? '#' + c : '#fff'; if (!c) swatch.style.border = '2px solid #ccc'; swatch.setAttribute('data-color', c); swatch.title = c ? '#' + c : 'Keine'; swatch.addEventListener('click', function() { swatchContainer.querySelectorAll('.pv-color-swatch').forEach(function(s) { s.classList.remove('selected'); }); swatch.classList.add('selected'); hiddenInput.value = c; }); swatchContainer.appendChild(swatch); }); // Load categories for parent dropdown ajaxCall('get_categories', {}, function(response) { var select = document.getElementById('pv-cat-edit-parent'); var opt = document.createElement('option'); opt.value = '0'; opt.textContent = '(' + (pvLang.none || 'Keine') + ')'; select.appendChild(opt); if (response.success && response.categories) { response.categories.forEach(function(cat) { // Don't show the category itself as a parent option if (cat.id == category.id) return; var o = document.createElement('option'); o.value = cat.id; o.textContent = cat.fullpath; if (cat.id == category.fk_parent) o.selected = true; select.appendChild(o); }); } }); // Focus label field document.getElementById('pv-cat-edit-label').focus(); // Save handler dialog.querySelector('.pv-btn-save').addEventListener('click', function() { var label = document.getElementById('pv-cat-edit-label').value.trim(); if (!label) { showToast('Bezeichnung ist erforderlich', 'error'); return; } var colorHex = document.getElementById('pv-cat-edit-color').value || ''; var data = { cat_label: label, cat_description: document.getElementById('pv-cat-edit-desc').value.trim(), cat_color: colorHex, cat_parent: document.getElementById('pv-cat-edit-parent').value }; var action = isNew ? 'create_category' : 'update_category'; if (!isNew) { data.category_id = category.id; } var saveBtn = dialog.querySelector('.pv-btn-save'); saveBtn.textContent = pvLang.saving || 'Speichert...'; saveBtn.disabled = true; ajaxCall(action, data, function(response) { if (response.success) { overlay.remove(); showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); setTimeout(function() { location.reload(); }, 800); } else { saveBtn.textContent = isNew ? (pvLang.create || 'Erstellen') : (pvLang.save || 'Speichern'); saveBtn.disabled = false; showToast(response.error || 'Fehler', 'error'); } }, function() { saveBtn.textContent = isNew ? (pvLang.create || 'Erstellen') : (pvLang.save || 'Speichern'); saveBtn.disabled = false; showToast('Fehler', 'error'); }); }); // Cancel / close dialog.querySelector('.pv-btn-cancel').addEventListener('click', function() { overlay.remove(); }); overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); }); dialog.addEventListener('keydown', function(e) { if (e.key === 'Escape') overlay.remove(); }); } // === AJAX Helper === function ajaxCall(action, data, onSuccess, onError) { var formData = new FormData(); formData.append('action', action); formData.append('token', pvToken); for (var key in data) { if (data.hasOwnProperty(key)) { formData.append(key, data[key]); } } fetch(pvAjaxUrl, { method: 'POST', body: formData }) .then(function(response) { return response.json(); }) .then(function(json) { if (onSuccess) onSuccess(json); }) .catch(function(err) { console.error('PV AJAX Error:', err); if (onError) onError(err); }); } });