- Kategorie-Baum mit Farben, Auf-/Zuklappen und Inline-Bearbeitung - AJAX-Handler für Produkt- und Kategorieaktionen (Best EK, Status-Toggle) - Admin: Ref-Schema und Standard-Aufklapp-Einstellung - CSS/JS erweitert für Baumansicht und modale Dialoge - bin/ zu .gitignore hinzugefügt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
806 lines
27 KiB
JavaScript
Executable file
806 lines
27 KiB
JavaScript
Executable file
/**
|
|
* 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 = '<h3>' + (pvLang.selectCategory || 'Kategorie zuweisen') + '</h3>' +
|
|
'<p>' + productRef + '</p>' +
|
|
'<select id="pv-cat-select"></select>' +
|
|
'<div class="pv-dialog-buttons">' +
|
|
'<button class="button" onclick="this.closest(\'.pv-dialog-overlay\').remove()">' + (pvLang.cancel || 'Abbrechen') + '</button>' +
|
|
'<button class="button btnprimary" id="pv-cat-assign-btn">' + (pvLang.assign || 'Zuweisen') + '</button>' +
|
|
'</div>';
|
|
|
|
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 =
|
|
'<h3><span class="fas fa-pencil-alt"></span> ' + (pvLang.editProduct || 'Produkt bearbeiten') + '</h3>' +
|
|
'<div class="pv-edit-form">' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.ref || 'Referenz') + '</label>' +
|
|
'<input type="text" id="pv-edit-ref" value="' + escHtml(product.ref) + '" style="text-transform:uppercase;font-family:monospace;font-weight:600;">' +
|
|
'</div>' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.label || 'Bezeichnung') + '</label>' +
|
|
'<input type="text" id="pv-edit-label" value="' + escHtml(product.label) + '">' +
|
|
'</div>' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.description || 'Beschreibung') + '</label>' +
|
|
'<textarea id="pv-edit-description" rows="2" style="width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #ccc;border-radius:3px;font-size:0.95em;">' + escHtml(product.description || '') + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.sellingPriceNet || 'VK netto') + '</label>' +
|
|
'<input type="text" id="pv-edit-sell-price" value="' + formatNum(product.sell_price) + '" class="pv-price-input" style="max-width:200px;">' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="pv-dialog-buttons">' +
|
|
'<a href="' + doliUrl + '" onclick="return pvOpenProductWindow(this.href)" class="button pv-btn-card" title="' + (pvLang.openProductCard || 'Produktkarte öffnen') + '">' +
|
|
'<span class="fas fa-external-link-alt"></span> ' + (pvLang.openProductCard || 'Produktkarte') +
|
|
'</a>' +
|
|
'<span class="pv-dialog-spacer"></span>' +
|
|
'<button type="button" class="button pv-btn-cancel">' + (pvLang.cancel || 'Abbrechen') + '</button>' +
|
|
'<button type="button" class="button btnprimary pv-btn-save">' + (pvLang.save || 'Speichern') + '</button>' +
|
|
'</div>';
|
|
|
|
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, '>').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 =
|
|
'<h3><span class="fas fa-folder"></span> ' + escHtml(title) + '</h3>' +
|
|
'<div class="pv-edit-form">' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.categoryLabel || 'Bezeichnung') + '</label>' +
|
|
'<input type="text" id="pv-cat-edit-label" value="' + escHtml(category.label) + '">' +
|
|
'</div>' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.categoryDescription || 'Beschreibung') + '</label>' +
|
|
'<textarea id="pv-cat-edit-desc" rows="3" style="width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #ccc;border-radius:3px;">' + escHtml(category.description || '') + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="pv-form-row-half">' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.categoryColor || 'Farbe') + '</label>' +
|
|
'<input type="hidden" id="pv-cat-edit-color" value="' + (colorVal || '') + '">' +
|
|
'<div class="pv-color-swatches" id="pv-color-swatches"></div>' +
|
|
'</div>' +
|
|
'<div class="pv-form-row">' +
|
|
'<label>' + (pvLang.parentCategory || 'Übergeordnete Kategorie') + '</label>' +
|
|
'<select id="pv-cat-edit-parent" style="width:100%;padding:7px 10px;"></select>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="pv-dialog-buttons">' +
|
|
'<span class="pv-dialog-spacer"></span>' +
|
|
'<button type="button" class="button pv-btn-cancel">' + (pvLang.cancel || 'Abbrechen') + '</button>' +
|
|
'<button type="button" class="button btnprimary pv-btn-save">' + (isNew ? (pvLang.create || 'Erstellen') : (pvLang.save || 'Speichern')) + '</button>' +
|
|
'</div>';
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
});
|