dolibarr.produktverwaltung/js/produktverwaltung.js
data 4b7046132c feat: Initiales Produktverwaltung-Modul (v1.0)
Kategorie-Baumansicht mit Inline-Editing für Dolibarr.
- Hierarchischer Kategoriebaum mit Auf-/Zuklappen
- Inline-Editing: Ref, Label, Beschreibung per Doppelklick
- Best EK mit 3-Zeichen Lieferanten-Badge
- Marge-Berechnung mit Farbmarkierung
- Kategorie-CRUD mit 20 Farb-Swatches
- Produkte ohne Kategorie Sektion
- Admin: Ref-Schema, Standard-Aufklapp-Verhalten
- Export: CSV und PDF
- Berechtigungen: read, write, delete, export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:33:47 +01:00

759 lines
25 KiB
JavaScript

/**
* 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');
});
}
// === 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', or 'description'
var productId = el.dataset.productId;
var currentValue = el.textContent.trim();
var input = document.createElement('input');
input.type = 'text';
input.className = 'pv-edit-input';
input.value = currentValue;
if (field === 'ref') {
input.style.textTransform = 'uppercase';
}
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;
}
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() {
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// === 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);
});
}
});