Files
kerimatilganandClaude Opus 4.7 395fadc83b Fix hardware-pricing reveal so it respects the Tailwind grid layout
The wizard's EK / Kundenverkaufspreis fields live inside a
<div class="grid grid-cols-2 gap-md"> wrapper. Toggling its inline
display:block as the previous code did replaced the grid layout and
stacked the two inputs full-width. Switch the wrapper to start with
the Tailwind `hidden` utility and toggle `hidden`/`grid` on the
classList, so when hardware is filled in the two pricing fields
appear side by side. Functionality and the existing commission
formula (Provision + Aktionscodes − EK + VK, already covered by the
provisionService tests) are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:38:28 +02:00

4259 lines
171 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Global variables
let currentSchaltungId = null;
let uploadedFiles = [];
let currentUser = null;
// Escape user-controlled values before embedding into innerHTML. Replaces all five
// XSS-relevant characters; safe inside both text content and double-quoted attributes.
function escapeHtml(value) {
if (value === null || value === undefined) return '';
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Initialize the application with background loading
document.addEventListener('DOMContentLoaded', async function() {
// Verify session before loading anything else; redirect to /login on 401.
try {
const meRes = await fetch('/api/auth/me', { credentials: 'same-origin' });
if (meRes.status === 401) {
window.location.href = '/login';
return;
}
currentUser = await meRes.json();
renderUserBadge();
} catch (e) {
window.location.href = '/login';
return;
}
loadAnbieter();
loadSchaltungen();
setupFileUpload();
renderWizardAnbieterButtons();
// Set today's date as default
const today = new Date().toISOString().split('T')[0];
document.getElementById('eingabeDatum').value = today;
document.getElementById('schaltungsDatum').value = today;
// Erfasser autofill
const erfasser = document.getElementById('wizardErfasser');
if (erfasser) erfasser.value = currentUser.displayName || currentUser.username;
document.getElementById('schaltungForm').addEventListener('submit', handleSchaltungSubmit);
preloadAllData();
});
// --- Schaltung Wizard ---
async function renderWizardAnbieterButtons() {
const container = document.getElementById('wizardAnbieterButtons');
if (!container) return;
container.innerHTML = '<div class="text-body-sm text-on-surface-variant col-span-full">Lade...</div>';
try {
const anbieter = await apiRequest('/api/anbieter');
container.innerHTML = '';
if (!anbieter.length) {
container.innerHTML = '<div class="text-body-sm text-on-surface-variant col-span-full">Keine Anbieter vorhanden. Bitte zuerst unter Administration einen Anbieter anlegen.</div>';
return;
}
anbieter.forEach(a => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'group flex flex-col items-center justify-center p-lg border border-outline-variant rounded bg-surface-container-lowest hover:-translate-y-1 hover:shadow-sm hover:border-primary transition-all duration-200 h-32';
btn.innerHTML = `
<span class="material-symbols-outlined text-4xl text-on-surface-variant group-hover:text-primary mb-sm" style="font-variation-settings: 'FILL' 1;">cell_tower</span>
<span class="text-title-sm text-on-surface"></span>
`;
btn.querySelector('span.text-title-sm').textContent = a.name;
btn.addEventListener('click', () => wizardSelectAnbieter(a.id, a.name, a.default_vo_id));
container.appendChild(btn);
});
} catch (e) {
container.innerHTML = '<div class="text-body-sm text-error col-span-full">Fehler beim Laden der Anbieter</div>';
}
}
async function wizardSelectAnbieter(anbieterId, anbieterName, defaultVoId) {
document.getElementById('anbieter').value = anbieterId;
document.getElementById('wizardAnbieterLabel').textContent = ' (' + anbieterName + ')';
document.getElementById('wizardStep1').style.display = 'none';
document.getElementById('wizardStep2').style.display = 'block';
const voSelect = document.getElementById('wizardVo');
voSelect.innerHTML = '<option value="">VO wählen...</option>';
document.getElementById('wizardTarif').innerHTML = '<option value="">Erst VO wählen...</option>';
document.getElementById('tarifoptionenSection').style.display = 'none';
try {
const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`);
vos.forEach(v => {
const label = `${v.nummer}${v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''}`;
voSelect.innerHTML += `<option value="${v.id}">${escapeHtml(label)}</option>`;
});
if (vos.length === 0) {
showMessage('Für diesen Anbieter wurden noch keine VOs angelegt. Bitte in der Administration → VOs eine VO anlegen.', 'error');
return;
}
// Preselect the provider's default VO if set, otherwise the first VO if it is the only one.
let preselect = null;
if (defaultVoId && vos.some(v => v.id === defaultVoId)) {
preselect = defaultVoId;
} else if (vos.length === 1) {
preselect = vos[0].id;
}
if (preselect) {
voSelect.value = String(preselect);
await loadTarifeForWizard();
}
} catch (e) { /* error shown */ }
}
function wizardBackToStep1() {
document.getElementById('wizardStep2').style.display = 'none';
document.getElementById('wizardStep1').style.display = 'block';
}
async function loadTarifeForWizard() {
const voId = document.getElementById('wizardVo').value;
const tarifSelect = document.getElementById('wizardTarif');
tarifSelect.innerHTML = '<option value="">Tarif wählen...</option>';
document.getElementById('tarifoptionenSection').style.display = 'none';
if (!voId) return;
try {
const tarife = await apiRequest(`/api/tarife?vo_id=${encodeURIComponent(voId)}`);
tarife.forEach(t => {
tarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
});
} catch (e) { /* error shown */ }
}
async function loadTarifoptionenForWizard() {
const tarifId = document.getElementById('wizardTarif').value;
const section = document.getElementById('tarifoptionenSection');
const list = document.getElementById('tarifoptionenList');
const noOptions = document.getElementById('noTarifoptionen');
list.innerHTML = '';
noOptions.style.display = 'none';
if (!tarifId) {
section.style.display = 'none';
return;
}
section.style.display = 'flex';
try {
const options = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`);
if (!options || options.length === 0) {
noOptions.style.display = 'block';
return;
}
options.forEach(opt => {
const wrapper = document.createElement('label');
wrapper.className = 'flex items-start gap-sm p-sm border border-outline-variant rounded-lg bg-surface-container-lowest hover:bg-surface-container-low cursor-pointer';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.name = 'tarifoptionen';
cb.value = opt.id;
cb.className = 'mt-0.5 text-primary rounded-sm border-outline-variant focus:ring-primary/20';
wrapper.appendChild(cb);
const body = document.createElement('div');
body.className = 'flex flex-col';
const nameEl = document.createElement('span');
nameEl.className = 'text-body-sm text-on-surface font-semibold';
nameEl.textContent = opt.name;
body.appendChild(nameEl);
if (opt.code) {
const codeEl = document.createElement('span');
codeEl.className = 'text-label-caps text-on-surface-variant';
codeEl.textContent = 'Code: ' + opt.code;
body.appendChild(codeEl);
}
if (opt.beschreibung) {
const desc = document.createElement('span');
desc.className = 'text-body-sm text-on-surface-variant';
desc.textContent = opt.beschreibung;
body.appendChild(desc);
}
wrapper.appendChild(body);
const price = document.createElement('span');
price.className = 'ml-auto font-label-numeric text-label-numeric ' + (opt.provision_euro >= 0 ? 'text-primary' : 'text-error');
price.textContent = (opt.provision_euro >= 0 ? '+' : '') + opt.provision_euro + ' €';
wrapper.appendChild(price);
list.appendChild(wrapper);
});
} catch (e) { /* error shown */ }
}
function renderUserBadge() {
if (!currentUser) return;
// Show admin-only nav items now that we know the role.
document.querySelectorAll('.admin-only').forEach(el => {
el.style.display = currentUser.isAdmin ? '' : 'none';
});
// Fill the user name in the new top header.
const nameEl = document.getElementById('headerUserName');
if (nameEl) nameEl.textContent = currentUser.displayName || currentUser.username;
// Wire the header logout button (idempotent: only attach once).
const logoutBtn = document.getElementById('headerLogoutBtn');
if (logoutBtn && !logoutBtn.dataset.wired) {
logoutBtn.dataset.wired = '1';
logoutBtn.addEventListener('click', async (e) => {
e.preventDefault();
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); } catch (_) {}
window.location.href = '/login';
});
}
// Remove the legacy floating badge if it was rendered by an older session.
const legacy = document.getElementById('userBadge');
if (legacy) legacy.remove();
}
// Preload all data in background
async function preloadAllData() {
console.log('Preloading all data in background...');
try {
// Load data for all tabs simultaneously (with delay to not block UI)
setTimeout(async () => {
const loadPromises = [
loadAdminData(), // Admin data
loadProvisionen(), // Provisionen tab
loadGeloeschteSchaltungen() // Gelöschte Schaltungen
];
// Execute all loads in parallel
await Promise.allSettled(loadPromises);
console.log('Background data loading completed');
// Show completion message briefly
showMessage('Hintergrunddaten erfolgreich geladen - Tab-Wechsel sind jetzt schneller! ⚡');
// Hide loading indicator if it exists
const loadingIndicator = document.getElementById('loadingIndicator');
if (loadingIndicator) {
loadingIndicator.style.display = 'none';
}
}, 1000); // Load after 1 second to not interfere with initial load
} catch (error) {
console.error('Error during background loading:', error);
}
}
// Tab switching
function showTab(tabName) {
// Hide all tabs
const tabs = document.querySelectorAll('.tab-content');
tabs.forEach(tab => tab.classList.remove('active'));
const navTabs = document.querySelectorAll('.nav-tab');
navTabs.forEach(tab => tab.classList.remove('active'));
// Show selected tab
document.getElementById(tabName).classList.add('active');
if (event && event.target) {
event.target.classList.add('active');
}
// Optimized loading - only reload if necessary (data should be preloaded)
if (tabName === 'overview') {
// Always refresh main data as it changes most frequently
loadSchaltungen();
} else if (tabName === 'provisionen') {
// Check if data is already loaded, if not load it
const provisionenTable = document.querySelector('#provisionenTable tbody');
if (!provisionenTable || !provisionenTable.innerHTML.trim()) {
loadProvisionen();
}
} else if (tabName === 'geloeschte') {
// Always refresh — the archive is rarely shown so this is cheap.
loadGeloeschteSchaltungen();
} else if (tabName === 'admin') {
// Check if admin data is already loaded
if (!document.getElementById('anbieterList') || !document.getElementById('anbieterList').innerHTML.trim()) {
loadAdminData();
}
} else if (tabName === 'users') {
loadUsers();
}
}
// API Helper functions
async function apiRequest(url, options = {}) {
try {
const opts = Object.assign({ credentials: 'same-origin' }, options);
const response = await fetch(url, opts);
// Session expired or unauthenticated -> back to login
if (response.status === 401) {
window.location.href = '/login';
throw new Error('Nicht angemeldet');
}
const contentType = response.headers.get('content-type');
let data = null;
if (contentType && contentType.includes('application/json')) {
const text = await response.text();
if (text.trim()) {
data = JSON.parse(text);
}
}
if (!response.ok) {
throw new Error(data?.error || `HTTP ${response.status}: ${response.statusText}`);
}
return data;
} catch (error) {
showMessage('Fehler: ' + error.message, 'error');
throw error;
}
}
// Message display as overlay
function showMessage(message, type = 'success') {
// Create overlay container if it doesn't exist
let overlay = document.getElementById('message-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'message-overlay';
overlay.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
pointer-events: none;
max-width: 400px;
`;
document.body.appendChild(overlay);
}
// Create message element
const messageDiv = document.createElement('div');
messageDiv.className = `message-toast ${type}`;
messageDiv.textContent = message;
messageDiv.style.cssText = `
background: ${type === 'success' ? '#d4edda' : '#f8d7da'};
border: 1px solid ${type === 'success' ? '#c3e6cb' : '#f5c6cb'};
color: ${type === 'success' ? '#155724' : '#721c24'};
padding: 12px 16px;
margin-bottom: 10px;
border-radius: 6px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
pointer-events: auto;
animation: slideInRight 0.3s ease-out;
font-family: Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
transition: opacity 0.3s ease-out;
`;
// Add CSS animation if not already added
if (!document.getElementById('message-animations')) {
const style = document.createElement('style');
style.id = 'message-animations';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
// Click to dismiss
messageDiv.addEventListener('click', () => {
messageDiv.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => messageDiv.remove(), 300);
});
overlay.appendChild(messageDiv);
// Auto-remove after 3 seconds
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => messageDiv.remove(), 300);
}
}, 3000);
}
// Load Anbieter
async function loadAnbieter() {
try {
const anbieter = await apiRequest('/api/anbieter');
// Note: #anbieter is now a hidden input populated by the wizard;
// we keep the helper only for selects that still exist (admin form).
const tarifAnbieterSelect = document.getElementById('tarifAnbieter');
if (tarifAnbieterSelect) {
tarifAnbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
tarifAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
}
// Refresh wizard buttons in case anbieters changed
renderWizardAnbieterButtons();
} catch (error) {
console.error('Fehler beim Laden der Anbieter:', error);
}
}
// Load Tarife for selected Anbieter
async function loadTarife() {
const anbieterId = document.getElementById('anbieter').value;
const tarifSelect = document.getElementById('tarif');
// Hide tarifoptionen section when anbieter changes
hideTarifoptionen();
if (!anbieterId) {
tarifSelect.innerHTML = '<option value="">Erst Anbieter wählen...</option>';
return;
}
try {
const tarife = await apiRequest(`/api/tarife/${anbieterId}`);
tarifSelect.innerHTML = '<option value="">Bitte wählen...</option>';
tarife.forEach(t => {
tarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Tarife:', error);
tarifSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
}
}
// Load tarifoptionen when tarif is selected
async function loadTarifoptionen() {
const tarifSelect = document.getElementById('tarif');
const tarifId = tarifSelect.value;
if (!tarifId) {
hideTarifoptionen();
return;
}
try {
const tarifoptionen = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`);
displayTarifoptionen(tarifoptionen);
} catch (error) {
console.error('Fehler beim Laden der Tarifoptionen:', error);
hideTarifoptionen();
}
}
function displayTarifoptionen(tarifoptionen) {
const section = document.getElementById('tarifoptionenSection');
const list = document.getElementById('tarifoptionenList');
const noOptions = document.getElementById('noTarifoptionen');
// Clear existing options
list.innerHTML = '';
if (tarifoptionen.length === 0) {
section.style.display = 'block';
list.style.display = 'none';
noOptions.style.display = 'block';
} else {
section.style.display = 'block';
list.style.display = 'grid';
noOptions.style.display = 'none';
tarifoptionen.forEach(option => {
const optionDiv = document.createElement('div');
optionDiv.style.cssText = 'padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #f9f9f9;';
optionDiv.innerHTML = `
<label style="display: flex; align-items: flex-start; gap: 8px; cursor: pointer;">
<input type="checkbox" name="tarifoptionen" value="${option.id}" style="margin-top: 2px;">
<div>
<div style="font-weight: bold; margin-bottom: 4px;">${escapeHtml(option.name)}</div>
${option.code ? `<div style="font-size: 0.9em; color: #666; margin-bottom: 4px;">Code: ${escapeHtml(option.code)}</div>` : ''}
${option.beschreibung ? `<div style="font-size: 0.8em; color: #555;">${escapeHtml(option.beschreibung)}</div>` : ''}
</div>
</label>
`;
list.appendChild(optionDiv);
});
}
}
function hideTarifoptionen() {
const section = document.getElementById('tarifoptionenSection');
section.style.display = 'none';
}
// Handle Schaltung form submission
async function handleSchaltungSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const schaltungData = Object.fromEntries(formData.entries());
// Wizard fields not part of FormData (hidden input has it but vo is on a non-named select).
schaltungData.vo_id = document.getElementById('wizardVo').value || null;
const selectedTarifoptionen = Array.from(document.querySelectorAll('input[name="tarifoptionen"]:checked'))
.map(checkbox => parseInt(checkbox.value));
if (selectedTarifoptionen.length) {
schaltungData.tarifoptionen = selectedTarifoptionen;
}
if (schaltungData.hardware && schaltungData.hardware.length !== 15) {
showMessage('Hardware muss genau 15 Zeichen haben (oder leer lassen)', 'error');
return;
}
try {
const response = await apiRequest('/api/schaltungen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(schaltungData),
});
currentSchaltungId = response.id;
if (uploadedFiles.length > 0) {
await uploadDocuments(currentSchaltungId);
}
showMessage('Schaltung erfolgreich gespeichert!');
e.target.reset();
uploadedFiles = [];
document.getElementById('uploadedFiles').innerHTML = '';
hideTarifoptionen();
const today = new Date().toISOString().split('T')[0];
document.getElementById('eingabeDatum').value = today;
document.getElementById('schaltungsDatum').value = today;
// Erfasser wieder befüllen (reset löscht's nicht da readonly, aber safety)
const erfasser = document.getElementById('wizardErfasser');
if (erfasser) erfasser.value = currentUser.displayName || currentUser.username;
// Zurück zu Schritt 1 für die nächste Schaltung
wizardBackToStep1();
await refreshDataAfterSchaltungChange();
} catch (error) {
console.error('Fehler beim Speichern der Schaltung:', error);
}
}
// Add tarifoptionen to schaltung
async function addTarifoptionenToSchaltung(schaltungId, tarifoptionenIds) {
for (const tarifoptionId of tarifoptionenIds) {
try {
await apiRequest(`/api/schaltungen/${schaltungId}/tarifoptionen`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tarifoption_id: tarifoptionId })
});
} catch (error) {
console.warn(`Fehler beim Hinzufügen der Tarifoption ${tarifoptionId}:`, error);
}
}
}
// File upload setup
function setupFileUpload() {
const uploadArea = document.getElementById('fileUploadArea');
const fileInput = document.getElementById('documentUpload');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
}
function handleFiles(files) {
Array.from(files).forEach(file => {
if (file.size > 10 * 1024 * 1024) {
showMessage(`Datei ${file.name} ist zu groß (max. 10MB)`, 'error');
return;
}
uploadedFiles.push(file);
displayUploadedFile(file);
});
}
function displayUploadedFile(file) {
const filesDiv = document.getElementById('uploadedFiles');
const fileDiv = document.createElement('div');
fileDiv.className = 'document-item';
const span = document.createElement('span');
span.textContent = `${file.name} (${formatFileSize(file.size)})`;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn-danger';
btn.textContent = 'Entfernen';
btn.addEventListener('click', () => removeFile(file.name));
fileDiv.appendChild(span);
fileDiv.appendChild(btn);
filesDiv.appendChild(fileDiv);
}
function removeFile(fileName) {
uploadedFiles = uploadedFiles.filter(file => file.name !== fileName);
const filesDiv = document.getElementById('uploadedFiles');
const fileItems = filesDiv.querySelectorAll('.document-item');
fileItems.forEach(item => {
if (item.querySelector('span').textContent.startsWith(fileName)) {
item.remove();
}
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async function uploadDocuments(schaltungId) {
for (const file of uploadedFiles) {
const formData = new FormData();
formData.append('document', file);
try {
await fetch(`/api/schaltungen/${schaltungId}/dokumente`, {
method: 'POST',
body: formData
});
} catch (error) {
showMessage(`Fehler beim Hochladen von ${file.name}`, 'error');
}
}
}
// Refresh all related data when schaltungen change
async function refreshDataAfterSchaltungChange() {
// Always refresh schaltungen
await loadSchaltungen();
// Only refresh provisions if the tab has been accessed before (performance optimization)
const provisionenTable = document.querySelector('#provisionenTable tbody');
if (provisionenTable && provisionenTable.innerHTML.trim()) {
await loadProvisionen();
}
}
// Load Schaltungen overview
async function loadSchaltungen() {
try {
const schaltungen = await apiRequest('/api/schaltungen');
renderSchaltungenRows(schaltungen);
} catch (error) {
console.error('Fehler beim Laden der Schaltungen:', error);
}
}
// Render schaltungen rows in the Stitch overview table.
function renderSchaltungenRows(schaltungen) {
const tbody = document.querySelector('#schaltungenTable tbody');
if (!tbody) return;
tbody.innerHTML = '';
if (!schaltungen || !schaltungen.length) {
const tr = document.createElement('tr');
tr.innerHTML = '<td colspan="9" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Keine Schaltungen vorhanden</td>';
tbody.appendChild(tr);
return;
}
schaltungen.forEach(s => {
const tr = document.createElement('tr');
tr.className = 'border-b border-outline-variant/30 even:bg-surface-container-low/50 hover:bg-surface-variant/30 transition-colors';
tr.innerHTML = `
<td class="px-md py-xs font-label-numeric text-label-numeric text-right text-on-surface-variant">${escapeHtml(s.schaltungs_id)}</td>
<td class="px-md py-xs">
<div class="flex flex-col gap-0.5">
<span class="font-label-numeric text-[11px] text-on-surface-variant">E: ${escapeHtml(formatDate(s.eingabe_datum))}</span>
<span class="font-label-numeric text-[11px] text-primary">A: ${escapeHtml(formatDate(s.schaltungs_datum))}</span>
</div>
</td>
<td class="px-md py-xs">
<div class="font-semibold text-on-background">${escapeHtml((s.vorname || '') + ' ' + (s.nachname || ''))}</div>
<div class="text-[12px] text-on-surface-variant">${escapeHtml(s.firma || '–')}</div>
</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-on-surface-variant">${escapeHtml(s.rufnummer || '–')}</td>
<td class="px-md py-xs">
<span class="bg-primary/10 text-primary border border-primary/20 px-2 py-0.5 rounded-full text-label-caps">${escapeHtml(s.anbieter_name)}</span>
</td>
<td class="px-md py-xs">${escapeHtml(s.tarif_name)}</td>
<td class="px-md py-xs text-on-surface-variant">${escapeHtml(s.hardware || '–')}</td>
<td class="px-md py-xs text-center">
<button data-action="documents" data-id="${s.id}" title="Dokumente anzeigen"
class="bg-tertiary/10 text-tertiary hover:bg-tertiary/20 transition-colors rounded p-1 inline-flex items-center justify-center border border-tertiary/20">
<span class="material-symbols-outlined text-[16px]">description</span>
</button>
</td>
<td class="px-md py-xs">
<div class="flex items-center justify-center gap-sm">
<button data-action="edit" data-id="${s.id}" title="Bearbeiten"
class="text-outline hover:text-primary transition-colors">
<span class="material-symbols-outlined text-[18px]">edit</span>
</button>
<button data-action="delete" data-id="${s.id}" data-schaltungs-id="${escapeHtml(s.schaltungs_id)}" data-name="${escapeHtml((s.vorname || '') + ' ' + (s.nachname || ''))}" title="Löschen"
class="text-outline hover:text-error transition-colors">
<span class="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
</td>
`;
tr.querySelectorAll('button[data-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
const id = parseInt(btn.dataset.id, 10);
if (action === 'documents') showDocuments(id);
else if (action === 'edit') editSchaltung(id);
else if (action === 'delete') deleteSchaltung(id, btn.dataset.schaltungsId, btn.dataset.name);
});
});
tbody.appendChild(tr);
});
}
// Format any ISO-like date string (YYYY-MM-DD or full timestamp) as DD.MM.YYYY
// with leading zeros. Returns the original string unchanged if it cannot be parsed.
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
if (isNaN(date.getTime())) return String(dateString);
const dd = String(date.getDate()).padStart(2, '0');
const mm = String(date.getMonth() + 1).padStart(2, '0');
const yyyy = date.getFullYear();
return `${dd}.${mm}.${yyyy}`;
}
async function showDocuments(schaltungId) {
try {
const documents = await apiRequest(`/api/schaltungen/${schaltungId}/dokumente`);
const modal = document.getElementById('documentsModal');
const modalBody = document.getElementById('documentsModalBody');
modalBody.innerHTML = '';
if (documents.length === 0) {
const empty = document.createElement('div');
empty.className = 'flex flex-col items-center justify-center py-xl text-center gap-sm';
empty.innerHTML = `
<span class="material-symbols-outlined text-[40px] text-outline">draft</span>
<div class="text-body-md text-on-surface">Keine Dokumente vorhanden</div>
<div class="text-body-sm text-on-surface-variant">Für diese Schaltung wurden noch keine Dokumente hochgeladen.</div>
`;
modalBody.appendChild(empty);
} else {
documents.forEach(doc => {
const fileExt = doc.original_filename.split('.').pop().toLowerCase();
let iconName = 'description';
if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) iconName = 'image';
else if (['doc', 'docx'].includes(fileExt)) iconName = 'article';
const fileSize = doc.filesize ? `${(doc.filesize / 1024).toFixed(1)} KB` : 'Unbekannt';
const uploadDate = doc.uploaded_at ? formatDate(doc.uploaded_at) : 'Unbekannt';
const showView = fileExt === 'pdf';
const safeName = escapeHtml(doc.original_filename);
const card = document.createElement('div');
card.className = 'flex items-center gap-md p-md border border-outline-variant rounded-lg bg-surface-container-lowest hover:bg-surface-container-low transition-colors';
card.innerHTML = `
<span class="material-symbols-outlined text-primary text-[28px] shrink-0">${iconName}</span>
<div class="flex flex-col min-w-0 flex-1">
<span class="text-body-md text-on-surface truncate" title="${safeName}">${safeName}</span>
<span class="text-body-sm text-on-surface-variant">${escapeHtml(fileSize)} · Hochgeladen: ${escapeHtml(uploadDate)}</span>
</div>
<div class="flex items-center gap-xs shrink-0">
${showView ? `<button data-doc-action="view" data-id="${doc.id}" data-name="${safeName}" title="Anzeigen"
class="icon-btn text-outline hover:text-primary transition-colors p-1">
<span class="material-symbols-outlined text-[20px]">visibility</span>
</button>` : ''}
<button data-doc-action="download" data-id="${doc.id}" title="Download"
class="icon-btn text-outline hover:text-primary transition-colors p-1">
<span class="material-symbols-outlined text-[20px]">download</span>
</button>
<button data-doc-action="delete" data-id="${doc.id}" data-name="${safeName}" data-schaltung-id="${schaltungId}" title="Löschen"
class="icon-btn text-outline hover:text-error transition-colors p-1">
<span class="material-symbols-outlined text-[20px]">delete</span>
</button>
</div>
`;
card.querySelectorAll('button[data-doc-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.docAction;
const id = parseInt(btn.dataset.id, 10);
if (action === 'view') viewPdfDocument(id, btn.dataset.name);
else if (action === 'download') downloadDocument(id);
else if (action === 'delete') deleteDocument(id, btn.dataset.name, parseInt(btn.dataset.schaltungId, 10));
});
});
modalBody.appendChild(card);
});
}
const fileInput = document.getElementById('additionalDocumentUpload');
fileInput.onchange = function (event) {
uploadAdditionalDocuments(schaltungId, event.target.files);
};
modal.classList.remove('hidden');
modal.classList.add('flex');
} catch (error) {
console.error('Fehler beim Laden der Dokumente:', error);
showMessage('Fehler beim Laden der Dokumente', 'error');
}
}
function closeDocumentsModal() {
const modal = document.getElementById('documentsModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function downloadDocument(documentId) {
window.open(`/api/dokumente/${documentId}/download`, '_blank');
}
// Upload additional documents
async function uploadAdditionalDocuments(schaltungId, files) {
if (!files || files.length === 0) return;
const uploadPromises = Array.from(files).map(async (file) => {
const formData = new FormData();
formData.append('document', file);
try {
const response = await fetch(`/api/schaltungen/${schaltungId}/dokumente`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return { success: true, filename: result.filename };
} catch (error) {
console.error(`Fehler beim Hochladen von ${file.name}:`, error);
return { success: false, filename: file.name, error: error.message };
}
});
try {
const results = await Promise.all(uploadPromises);
const successCount = results.filter(r => r.success).length;
const failureCount = results.filter(r => !r.success).length;
if (successCount > 0) {
showMessage(`${successCount} Dokument(e) erfolgreich hochgeladen`, 'success');
// Refresh the documents display
showDocuments(schaltungId);
}
if (failureCount > 0) {
showMessage(`${failureCount} Dokument(e) konnten nicht hochgeladen werden`, 'error');
}
// Clear the file input
document.getElementById('additionalDocumentUpload').value = '';
} catch (error) {
console.error('Fehler beim Upload:', error);
showMessage('Fehler beim Hochladen der Dokumente', 'error');
}
}
// Delete document
async function deleteDocument(documentId, filename, schaltungId) {
if (!confirm(`Möchten Sie das Dokument "${filename}" wirklich löschen?`)) {
return;
}
try {
const response = await fetch(`/api/dokumente/${documentId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
showMessage('Dokument erfolgreich gelöscht', 'success');
// Refresh the documents display
showDocuments(schaltungId);
} catch (error) {
console.error('Fehler beim Löschen:', error);
showMessage('Fehler beim Löschen des Dokuments', 'error');
}
}
// Close modal when clicking outside
document.addEventListener('click', function(event) {
const modal = document.getElementById('documentsModal');
if (event.target === modal) {
closeDocumentsModal();
}
});
// Admin functions
// New Admin Category Navigation
function showAdminCategory(category) {
// Hide all categories
document.querySelectorAll('.admin-category').forEach(cat => {
cat.classList.remove('active');
});
// Remove active class from all buttons
document.querySelectorAll('.admin-cat-btn').forEach(btn => {
btn.classList.remove('active');
});
// Show selected category
document.getElementById(`admin-${category}`).classList.add('active');
// Add active class to clicked button
event.target.closest('.admin-cat-btn').classList.add('active');
// Load data for the category
switch(category) {
case 'anbieter':
loadAnbieterAdmin();
break;
case 'vos':
loadVosAdmin();
loadAnbieterForVoForm();
break;
case 'tarife':
loadTarifeAdmin();
loadAnbieterForTarife();
break;
case 'tarifoptionen':
loadTarifoptionAnbieter();
displayTarifoptionenAdmin();
break;
case 'mitarbeiter-prozentsaetze':
loadMitarbeiterAnbieter();
loadMitarbeiterProzentsaetzeAdmin();
break;
case 'mitarbeiter-provisionen':
loadTarifProvisionsAdmin();
break;
}
}
async function loadAnbieterAdmin() {
try {
const anbieter = await apiRequest('/api/anbieter');
const tbody = document.getElementById('anbieterList');
tbody.innerHTML = '';
if (anbieter.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #666;">Keine Anbieter vorhanden</td></tr>';
return;
}
anbieter.forEach(a => {
const createdAt = formatDate(a.created_at);
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${a.id}</td>
<td>${escapeHtml(a.name)}</td>
<td>${escapeHtml(createdAt)}</td>
<td>
<button data-anbieter-action="delete" data-id="${a.id}" data-name="${escapeHtml(a.name)}" class="btn-danger">Löschen</button>
</td>
`;
tr.querySelector('button[data-anbieter-action="delete"]').addEventListener('click', (e) => {
const btn = e.currentTarget;
deleteAnbieter(parseInt(btn.dataset.id, 10), btn.dataset.name);
});
tbody.appendChild(tr);
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter-Administration:', error);
const tbody = document.getElementById('anbieterList');
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #666;">Fehler beim Laden der Daten</td></tr>';
}
}
// Prevent race conditions
let isLoadingTarifeAdmin = false;
async function loadTarifeAdmin() {
if (isLoadingTarifeAdmin) {
console.warn('loadTarifeAdmin already running, skipping to prevent race condition');
return;
}
try {
isLoadingTarifeAdmin = true;
// Use new API endpoint that gets all tarife with anbieter names
const tarife = await apiRequest('/api/tarife');
const tbody = document.getElementById('tarifeList');
tbody.innerHTML = '';
if (tarife.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: #666;">Keine Tarife vorhanden</td></tr>';
return;
}
tarife.forEach(t => {
const provision = `${(t.provision_euro || 0).toFixed(2).replace('.', ',')}€`;
const voLabel = t.vo_nummer
? `${t.vo_nummer}${t.vo_bezeichnung ? ' (' + t.vo_bezeichnung + ')' : ''}`
: '—';
const tr = document.createElement('tr');
tr.id = `tarif-item-${t.id}`;
tr.innerHTML = `
<td>${t.id}</td>
<td>${escapeHtml(t.anbieter_name || 'Unbekannt')}</td>
<td>${escapeHtml(voLabel)}</td>
<td>${escapeHtml(t.name)}</td>
<td>${escapeHtml(provision)}</td>
<td>${escapeHtml(t.beschreibung || '-')}</td>
<td>
<button data-tarif-action="edit" data-id="${t.id}" class="btn-success" style="margin-right: 5px;">Bearbeiten</button>
<button data-tarif-action="delete" data-id="${t.id}" data-name="${escapeHtml(t.name)}" class="btn-danger">Löschen</button>
</td>
`;
tr.querySelectorAll('button[data-tarif-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.tarifAction;
const id = parseInt(btn.dataset.id, 10);
if (action === 'edit') editTariff(id);
else if (action === 'delete') deleteTariff(id, btn.dataset.name);
});
});
tbody.appendChild(tr);
});
} catch (error) {
console.error('Fehler beim Laden der Tarif-Administration:', error);
const tbody = document.getElementById('tarifeList');
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: #666;">Fehler beim Laden der Daten</td></tr>';
} finally {
isLoadingTarifeAdmin = false;
}
}
async function addAnbieter() {
const name = document.getElementById('newAnbieter').value.trim();
if (!name) {
showMessage('Bitte einen Anbieter-Namen eingeben', 'error');
return;
}
try {
await apiRequest('/api/anbieter', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
});
document.getElementById('newAnbieter').value = '';
showMessage('Anbieter erfolgreich hinzugefügt');
// Reload data
await loadAnbieter();
await loadAnbieterAdmin();
} catch (error) {
console.error('Fehler beim Hinzufügen des Anbieters:', error);
}
}
// Load anbieter for tarife select
async function loadAnbieterForTarife() {
try {
const anbieter = await apiRequest('/api/anbieter');
const select = document.getElementById('tarifAnbieter');
select.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter für Tarife:', error);
}
}
// Load anbieter for mitarbeiter select
async function loadMitarbeiterAnbieter() {
try {
const anbieter = await apiRequest('/api/anbieter');
const select = document.getElementById('mitarbeiterAnbieter');
select.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter für Mitarbeiter:', error);
}
}
// Load anbieter for tarifoption select
async function loadTarifoptionAnbieter() {
try {
const anbieter = await apiRequest('/api/anbieter');
const select = document.getElementById('tarifoptionAnbieter');
select.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter für Tarifoptionen:', error);
}
}
async function addTarif() {
const name = document.getElementById('newTarif').value.trim();
const anbieterId = document.getElementById('tarifAnbieter').value;
const voId = document.getElementById('tarifVo').value;
const provisionEuro = document.getElementById('tarifProvisionBetrag').value;
const beschreibung = document.getElementById('tarifBeschreibung').value.trim();
if (!name || !anbieterId) {
showMessage('Bitte Tarif-Name und Anbieter auswählen', 'error');
return;
}
if (!voId) {
showMessage('Bitte VO auswählen', 'error');
return;
}
if (provisionEuro && parseFloat(provisionEuro) < 0) {
showMessage('Provision kann nicht negativ sein', 'error');
return;
}
try {
const requestData = {
name,
anbieter_id: anbieterId,
vo_id: voId,
provision_euro: provisionEuro ? parseFloat(provisionEuro) : 0,
beschreibung: beschreibung || null,
};
await apiRequest('/api/tarife', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData),
});
document.getElementById('newTarif').value = '';
document.getElementById('tarifAnbieter').value = '';
document.getElementById('tarifVo').innerHTML = '<option value="">VO wählen...</option>';
document.getElementById('tarifProvisionBetrag').value = '';
document.getElementById('tarifBeschreibung').value = '';
showMessage('Tarif erfolgreich hinzugefügt');
await loadTarifeAdmin();
await loadTarifeForProvision();
} catch (error) {
console.error('Fehler beim Hinzufügen des Tarifs:', error);
}
}
// Populates the VO dropdown in the add-tariff form based on the selected provider.
async function loadVosForTarifForm() {
const anbieterId = document.getElementById('tarifAnbieter').value;
const select = document.getElementById('tarifVo');
select.innerHTML = '<option value="">VO wählen...</option>';
if (!anbieterId) return;
try {
const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`);
vos.forEach(v => {
const label = `${v.nummer}${v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''}`;
select.innerHTML += `<option value="${v.id}">${escapeHtml(label)}</option>`;
});
} catch (e) { /* error already shown */ }
}
// Commission management functions
async function loadProvisionen() {
try {
const data = await apiRequest('/api/provisionen');
// Store data globally for filtering
allProvisionen = data;
// Update filter dropdowns
await updateFilterDropdowns();
// Apply current filters (which will also update the display)
applyFiltersProvisionen();
} catch (error) {
console.error('Fehler beim Laden der Provisionen:', error);
}
}
async function loadAdminData() {
await loadAnbieterAdmin();
await loadTarifeAdmin();
await loadGlobalProvisionsregelnAdmin();
await loadProvisionsregelnAdmin();
await loadTarifeForProvision();
await loadGlobalProvisionAnbieter();
await loadTarifoptionenAdmin();
}
async function loadTarifeForProvision() {
try {
const anbieter = await apiRequest('/api/anbieter');
const provisionTarifSelect = document.getElementById('provisionTarif');
if (!provisionTarifSelect) return;
provisionTarifSelect.innerHTML = '<option value="">Tarif wählen...</option>';
for (const a of anbieter) {
const tarife = await apiRequest(`/api/tarife/${a.id}`);
if (tarife.length > 0) {
tarife.forEach(t => {
provisionTarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(a.name + ' - ' + t.name)}</option>`;
});
}
}
} catch (error) {
console.error('Fehler beim Laden der Tarife für Provisionen:', error);
}
}
async function loadProvisionsregelnAdmin() {
try {
const anbieter = await apiRequest('/api/anbieter');
const provisionsregelnList = document.getElementById('provisionsregelnList');
if (!provisionsregelnList) return;
provisionsregelnList.innerHTML = '';
for (const a of anbieter) {
const tarife = await apiRequest(`/api/tarife/${a.id}`);
for (const t of tarife) {
const regeln = await apiRequest(`/api/tarife/${t.id}/provisionsregeln`);
if (regeln.length > 0) {
const providerDiv = document.createElement('div');
const h4 = document.createElement('h4');
h4.textContent = `${a.name} - ${t.name}`;
providerDiv.appendChild(h4);
provisionsregelnList.appendChild(providerDiv);
regeln.forEach(r => {
const item = document.createElement('div');
item.className = 'list-item';
item.style.marginLeft = '20px';
const typText = r.provision_typ === 'fix' ? `${r.betrag} €` : `${r.betrag} %`;
const statusText = r.aktiv ? 'Aktiv' : 'Inaktiv';
item.innerHTML = `
<span>${escapeHtml(typText)} - ${escapeHtml(r.beschreibung || 'Keine Beschreibung')} (${escapeHtml(statusText)})</span>
<div>
<button data-regel-action="toggle" data-id="${r.id}" data-aktiv="${!r.aktiv}" class="btn-${r.aktiv ? 'danger' : 'success'}">
${r.aktiv ? 'Deaktivieren' : 'Aktivieren'}
</button>
</div>
`;
item.querySelector('button[data-regel-action]').addEventListener('click', (e) => {
const btn = e.currentTarget;
toggleProvisionsregel(parseInt(btn.dataset.id, 10), btn.dataset.aktiv === 'true');
});
provisionsregelnList.appendChild(item);
});
}
}
}
} catch (error) {
console.error('Fehler beim Laden der Provisionsregeln-Administration:', error);
}
}
async function addProvisionsregel() {
const tarifId = document.getElementById('provisionTarif').value;
const typ = document.getElementById('provisionTyp').value;
const betrag = document.getElementById('provisionBetrag').value;
const beschreibung = document.getElementById('provisionBeschreibung').value;
if (!tarifId || !typ || !betrag) {
showMessage('Bitte alle Pflichtfelder ausfüllen', 'error');
return;
}
if (typ === 'prozent' && (parseFloat(betrag) < 0 || parseFloat(betrag) > 100)) {
showMessage('Prozentual muss zwischen 0 und 100 sein', 'error');
return;
}
if (parseFloat(betrag) < 0) {
showMessage('Betrag kann nicht negativ sein', 'error');
return;
}
try {
await apiRequest('/api/provisionsregeln', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tarif_id: tarifId,
provision_typ: typ,
betrag: parseFloat(betrag),
beschreibung: beschreibung || null
})
});
// Reset form
document.getElementById('provisionTarif').value = '';
document.getElementById('provisionTyp').value = '';
document.getElementById('provisionBetrag').value = '';
document.getElementById('provisionBeschreibung').value = '';
showMessage('Provisionsregel erfolgreich hinzugefügt');
// Reload data
await loadProvisionsregelnAdmin();
} catch (error) {
console.error('Fehler beim Hinzufügen der Provisionsregel:', error);
}
}
async function toggleProvisionsregel(id, aktiv) {
try {
await apiRequest(`/api/provisionsregeln/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
aktiv: aktiv
})
});
showMessage(`Provisionsregel erfolgreich ${aktiv ? 'aktiviert' : 'deaktiviert'}`);
// Reload data
await loadProvisionsregelnAdmin();
} catch (error) {
console.error('Fehler beim Ändern der Provisionsregel:', error);
showMessage('Fehler beim Ändern der Provisionsregel', 'error');
}
}
// Global Provider Commission Rules Functions
async function loadGlobalProvisionAnbieter() {
try {
const anbieter = await apiRequest('/api/anbieter');
const globalProvisionAnbieterSelect = document.getElementById('globalProvisionAnbieter');
if (!globalProvisionAnbieterSelect) return;
globalProvisionAnbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
globalProvisionAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter für globale Provisionen:', error);
}
}
async function loadGlobalProvisionsregelnAdmin() {
try {
const anbieter = await apiRequest('/api/anbieter');
const globalProvisionsregelnList = document.getElementById('globalProvisionsregelnList');
if (!globalProvisionsregelnList) return;
globalProvisionsregelnList.innerHTML = '';
for (const a of anbieter) {
const regeln = await apiRequest(`/api/anbieter/${a.id}/provisionsregeln`);
if (regeln.length > 0) {
const providerDiv = document.createElement('div');
const h4 = document.createElement('h4');
h4.textContent = `Globale Regeln für ${a.name}`;
providerDiv.appendChild(h4);
globalProvisionsregelnList.appendChild(providerDiv);
regeln.forEach(r => {
const item = document.createElement('div');
item.className = 'list-item';
item.style.marginLeft = '20px';
let displayText = 'Keine Mitarbeiter-Provision definiert';
if (r.verkaeufer_provision_typ && r.verkaeufer_provision_betrag) {
const mitarbeiterText = r.verkaeufer_provision_typ === 'fix'
? `${r.verkaeufer_provision_betrag} €`
: `${r.verkaeufer_provision_betrag} %`;
displayText = `Mitarbeiter: ${mitarbeiterText}`;
if (r.verkaeufer_provision_beschreibung) {
displayText += ` - ${r.verkaeufer_provision_beschreibung}`;
}
}
const statusText = r.aktiv ? 'Aktiv' : 'Inaktiv';
item.innerHTML = `
<span>${escapeHtml(displayText)} (${escapeHtml(statusText)})</span>
<div>
<button data-global-action="toggle" data-id="${r.id}" data-aktiv="${!r.aktiv}" class="btn-${r.aktiv ? 'danger' : 'success'}">
${r.aktiv ? 'Deaktivieren' : 'Aktivieren'}
</button>
</div>
`;
item.querySelector('button[data-global-action]').addEventListener('click', (e) => {
const btn = e.currentTarget;
toggleGlobalProvisionsregel(parseInt(btn.dataset.id, 10), btn.dataset.aktiv === 'true');
});
globalProvisionsregelnList.appendChild(item);
});
}
}
} catch (error) {
console.error('Fehler beim Laden der globalen Provisionsregeln-Administration:', error);
}
}
async function addGlobalProvisionsregel() {
const anbieterId = document.getElementById('globalProvisionAnbieter').value;
// Employee provision fields
const verkaeuferProvisionTyp = document.getElementById('globalVerkaeuferProvisionTyp').value;
const verkaeuferProvisionBetrag = document.getElementById('globalVerkaeuferProvisionBetrag').value;
const verkaeuferProvisionBeschreibung = document.getElementById('globalVerkaeuferProvisionBeschreibung').value;
if (!anbieterId || !verkaeuferProvisionTyp || !verkaeuferProvisionBetrag) {
showMessage('Bitte alle Pflichtfelder ausfüllen', 'error');
return;
}
if (verkaeuferProvisionTyp === 'prozent' && (parseFloat(verkaeuferProvisionBetrag) < 0 || parseFloat(verkaeuferProvisionBetrag) > 100)) {
showMessage('Mitarbeiter-Provision Prozentual muss zwischen 0 und 100 sein', 'error');
return;
}
if (parseFloat(verkaeuferProvisionBetrag) < 0) {
showMessage('Mitarbeiter-Provision kann nicht negativ sein', 'error');
return;
}
try {
const requestData = {
anbieter_id: anbieterId,
// For employee-only rules, we need a meaningful shop provision as base
provision_typ: 'prozent', // Use percentage as it's most flexible
betrag: 100, // 100% - this means shop gets the full tariff commission
beschreibung: 'Standard Anbieter-Provision (100% der Tarif-Provision)',
verkaeufer_provision_typ: verkaeuferProvisionTyp,
verkaeufer_provision_betrag: parseFloat(verkaeuferProvisionBetrag),
verkaeufer_provision_beschreibung: verkaeuferProvisionBeschreibung || null
};
await apiRequest('/api/anbieter-provisionsregeln', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
// Reset form
document.getElementById('globalProvisionAnbieter').value = '';
document.getElementById('globalVerkaeuferProvisionTyp').value = '';
document.getElementById('globalVerkaeuferProvisionBetrag').value = '';
document.getElementById('globalVerkaeuferProvisionBeschreibung').value = '';
showMessage('Globale Mitarbeiter-Provisionsregel erfolgreich hinzugefügt');
// Reload data
await loadGlobalProvisionsregelnAdmin();
} catch (error) {
console.error('Fehler beim Hinzufügen der globalen Provisionsregel:', error);
}
}
async function toggleGlobalProvisionsregel(id, aktiv) {
try {
await apiRequest(`/api/anbieter-provisionsregeln/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
aktiv: aktiv
})
});
showMessage(`Globale Provisionsregel erfolgreich ${aktiv ? 'aktiviert' : 'deaktiviert'}`);
// Reload data
await loadGlobalProvisionsregelnAdmin();
} catch (error) {
console.error('Fehler beim Ändern der globalen Provisionsregel:', error);
showMessage('Fehler beim Ändern der globalen Provisionsregel', 'error');
}
}
// Edit/Delete Functions for Tariffs and Schaltungen
// Modal Management
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
// Tariff Edit/Delete Functions
async function editTariff(id) {
try {
const tariff = await apiRequest(`/api/tarif/${id}`);
if (!tariff) {
showMessage('Tarif nicht gefunden', 'error');
return;
}
document.getElementById('editTariffId').value = tariff.id;
document.getElementById('editTariffName').value = tariff.name;
document.getElementById('editTariffProvisionBetrag').value = tariff.provision_euro || '';
// Populate VO dropdown with the tariff's provider VOs and preselect.
const voSelect = document.getElementById('editTariffVo');
voSelect.innerHTML = '<option value="">VO wählen...</option>';
const vos = await apiRequest(`/api/vos?anbieter_id=${tariff.anbieter_id}`);
vos.forEach(v => {
const label = `${v.nummer}${v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''}`;
const opt = document.createElement('option');
opt.value = v.id;
opt.textContent = label;
if (tariff.vo_id == v.id) opt.selected = true;
voSelect.appendChild(opt);
});
// Populate tarifoptionen checkbox grid (anbieter-wide options, with assigned flag)
const container = document.getElementById('editTariffOptionenContainer');
container.innerHTML = '';
const options = await apiRequest(`/api/tarife/${id}/tarifoptionen-multi`);
if (!options || options.length === 0) {
container.innerHTML = '<small style="color:#666;">Keine Tarifoptionen für diesen Anbieter vorhanden</small>';
} else {
options.forEach(o => {
const wrapper = document.createElement('label');
wrapper.style.cssText = 'display:flex; align-items:center; gap:8px; padding:4px 0; cursor:pointer;';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = o.id;
cb.checked = !!o.assigned;
cb.className = 'edit-tariff-option-checkbox';
wrapper.appendChild(cb);
const span = document.createElement('span');
const price = (o.provision_euro >= 0 ? '+' : '') + o.provision_euro + '€';
span.textContent = `${o.name}${o.code ? ' [' + o.code + ']' : ''} (${price})`;
wrapper.appendChild(span);
container.appendChild(wrapper);
});
}
document.getElementById('editTariffModal').style.display = 'block';
document.getElementById('editTariffForm').onsubmit = async (e) => {
e.preventDefault();
const checked = Array.from(container.querySelectorAll('.edit-tariff-option-checkbox:checked'))
.map(cb => parseInt(cb.value, 10));
const formData = {
name: document.getElementById('editTariffName').value.trim(),
vo_id: document.getElementById('editTariffVo').value || null,
provision_euro: document.getElementById('editTariffProvisionBetrag').value
? parseFloat(document.getElementById('editTariffProvisionBetrag').value)
: 0,
tarifoption_ids: checked,
};
try {
await apiRequest(`/api/tarife/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
closeModal('editTariffModal');
showMessage('Tarif erfolgreich aktualisiert');
await loadTarifeAdmin();
await loadTarifeForProvision();
} catch (error) {
console.error('Fehler beim Aktualisieren des Tarifs:', error);
}
};
} catch (error) {
console.error('Fehler beim Laden der Tarif-Daten:', error);
}
}
async function deleteTariff(id, name) {
if (!confirm(`Sind Sie sicher, dass Sie den Tarif "${name}" löschen möchten?\n\nHinweis: Tarife mit bestehenden Schaltungen können nicht gelöscht werden.`)) {
return;
}
try {
await apiRequest(`/api/tarife/${id}`, {
method: 'DELETE'
});
showMessage('Tarif erfolgreich gelöscht');
// Remove the deleted item from DOM immediately (much faster)
const deletedElement = document.getElementById(`tarif-item-${id}`);
if (deletedElement) {
deletedElement.remove();
}
} catch (error) {
console.error('Fehler beim Löschen des Tarifs:', error);
}
}
async function deleteAnbieter(id, name) {
if (!confirm(`Sind Sie sicher, dass Sie den Anbieter "${name}" löschen möchten?\n\nHinweis: Anbieter mit bestehenden Tarifen oder Schaltungen können nicht gelöscht werden.`)) {
return;
}
try {
await apiRequest(`/api/anbieter/${id}`, {
method: 'DELETE'
});
showMessage('Anbieter erfolgreich gelöscht');
await loadAnbieterAdmin();
await loadAnbieter();
await loadAnbieterForProvision();
} catch (error) {
console.error('Fehler beim Löschen des Anbieters:', error);
// Show more detailed error message if provided by server
if (error.message) {
showMessage(error.message, 'error');
}
}
}
// Schaltung Edit/Delete Functions
async function editSchaltung(id) {
try {
// Get schaltung data
const schaltungen = await apiRequest('/api/schaltungen');
const schaltung = schaltungen.find(s => s.id == id);
if (!schaltung) {
showMessage('Schaltung nicht gefunden', 'error');
return;
}
// Load anbieter for the modal
const anbieter = await apiRequest('/api/anbieter');
const editAnbieterSelect = document.getElementById('editAnbieter');
editAnbieterSelect.innerHTML = '<option value="">Bitte wählen...</option>';
anbieter.forEach(a => {
editAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
// Fill modal with schaltung data
document.getElementById('editSchaltungId').value = schaltung.id;
document.getElementById('editSchaltungsId').value = schaltung.schaltungs_id;
document.getElementById('editEingabeDatum').value = schaltung.eingabe_datum;
document.getElementById('editSchaltungsDatum').value = schaltung.schaltungs_datum;
document.getElementById('editVorname').value = schaltung.vorname;
document.getElementById('editNachname').value = schaltung.nachname;
document.getElementById('editFirma').value = schaltung.firma || '';
document.getElementById('editRufnummer').value = schaltung.rufnummer;
document.getElementById('editHardware').value = schaltung.hardware || '';
document.getElementById('editHardwareEk').value = schaltung.hardware_ek || '';
document.getElementById('editKundenverkaufspreis').value = schaltung.kundenverkaufspreis_netto || '';
// Toggle hardware pricing visibility
toggleEditHardwarePricing();
// Set anbieter and load tarife
editAnbieterSelect.value = schaltung.anbieter_id;
await loadTarifeForEdit();
document.getElementById('editTarif').value = schaltung.tarif_id;
// Load tarifoptionen for the selected tarif
await loadTarifoptionenForEdit();
// Load and check currently selected tarifoptionen
try {
const currentTarifoptionen = await apiRequest(`/api/schaltungen/${id}/tarifoptionen`);
console.log('Current tarifoptionen for editing:', currentTarifoptionen);
console.log('First option detailed:', JSON.stringify(currentTarifoptionen[0], null, 2));
// Store current tarifoptionen for later use
window.currentEditTarifoptionen = currentTarifoptionen;
// Wait a bit to ensure tarifoptionen are loaded
setTimeout(() => {
// Debug: Show all available checkboxes
const allCheckboxes = document.querySelectorAll('input[name="editTarifoptionen"]');
console.log('All available checkboxes:', Array.from(allCheckboxes).map(cb => cb.value));
// Check the checkboxes for currently selected options
currentTarifoptionen.forEach(option => {
// Try both possible field names
const optionId = option.tarifoption_id || option.id;
console.log(`Looking for checkbox with value: ${optionId} (from tarifoption_id: ${option.tarifoption_id}, id: ${option.id})`);
const checkbox = document.querySelector(`input[name="editTarifoptionen"][value="${optionId}"]`);
if (checkbox) {
console.log(`Found checkbox, checking it`);
checkbox.checked = true;
// Add visual indicator for currently selected options
const label = checkbox.closest('label');
if (label) {
label.style.backgroundColor = '#e8f5e8';
label.style.padding = '5px';
label.style.borderRadius = '3px';
label.style.fontWeight = 'bold';
}
} else {
console.log(`Checkbox not found for tarifoption_id: ${optionId}`);
}
});
}, 100);
} catch (error) {
console.error('Fehler beim Laden der aktuellen Tarifoptionen:', error);
}
// Show modal
document.getElementById('editSchaltungModal').style.display = 'block';
// Setup form submission
document.getElementById('editSchaltungForm').onsubmit = async (e) => {
e.preventDefault();
const hardware = document.getElementById('editHardware').value.trim();
const hardwareEk = document.getElementById('editHardwareEk').value;
const kundenverkaufspreis = document.getElementById('editKundenverkaufspreis').value;
// Validate hardware pricing
if (hardware && hardware !== '') {
if (!hardwareEk || !kundenverkaufspreis) {
showMessage('Wenn Hardware eingegeben wird, müssen Hardware-EK und Kundenverkaufspreis ausgefüllt werden', 'error');
return;
}
}
// Get selected tarifoptionen
const selectedTarifoptionen = [];
const checkboxes = document.querySelectorAll('input[name="editTarifoptionen"]:checked');
checkboxes.forEach(checkbox => {
selectedTarifoptionen.push(parseInt(checkbox.value));
});
const formData = {
schaltungs_id: document.getElementById('editSchaltungsId').value.trim(),
eingabe_datum: document.getElementById('editEingabeDatum').value,
schaltungs_datum: document.getElementById('editSchaltungsDatum').value,
vorname: document.getElementById('editVorname').value.trim(),
nachname: document.getElementById('editNachname').value.trim(),
firma: document.getElementById('editFirma').value.trim() || null,
rufnummer: document.getElementById('editRufnummer').value.trim(),
anbieter_id: parseInt(document.getElementById('editAnbieter').value),
tarif_id: parseInt(document.getElementById('editTarif').value),
hardware: hardware || null,
hardware_ek: hardware && hardwareEk ? parseFloat(hardwareEk) : null,
kundenverkaufspreis_netto: hardware && kundenverkaufspreis ? parseFloat(kundenverkaufspreis) : null,
tarifoptionen: selectedTarifoptionen
};
try {
await apiRequest(`/api/schaltungen/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
closeModal('editSchaltungModal');
showMessage('Schaltung erfolgreich aktualisiert');
await refreshDataAfterSchaltungChange();
} catch (error) {
console.error('Fehler beim Aktualisieren der Schaltung:', error);
}
};
} catch (error) {
console.error('Fehler beim Laden der Schaltungs-Daten:', error);
}
}
function toggleEditHardwarePricing() {
const hardwareInput = document.getElementById('editHardware');
const pricingFields = document.getElementById('editHardwarePricingFields');
const hardwareEkInput = document.getElementById('editHardwareEk');
const kundenverkaufspreisInput = document.getElementById('editKundenverkaufspreis');
if (hardwareInput.value.trim() !== '') {
pricingFields.style.display = 'block';
hardwareEkInput.required = true;
kundenverkaufspreisInput.required = true;
} else {
pricingFields.style.display = 'none';
hardwareEkInput.required = false;
kundenverkaufspreisInput.required = false;
hardwareEkInput.value = '';
kundenverkaufspreisInput.value = '';
}
}
async function loadTarifeForEdit() {
const anbieterId = document.getElementById('editAnbieter').value;
const tarifSelect = document.getElementById('editTarif');
if (!anbieterId) {
tarifSelect.innerHTML = '<option value="">Erst Anbieter wählen...</option>';
document.getElementById('editTarifoptionenContainer').innerHTML = '<div>Erst Anbieter wählen...</div>';
return;
}
try {
const tarife = await apiRequest(`/api/tarife/${anbieterId}`);
tarifSelect.innerHTML = '<option value="">Bitte wählen...</option>';
tarife.forEach(t => {
tarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
});
// Clear tarifoptionen when anbieter changes
document.getElementById('editTarifoptionenContainer').innerHTML = '<div>Erst Tarif wählen...</div>';
// Add event listener for tarif change
tarifSelect.onchange = loadTarifoptionenForEdit;
} catch (error) {
console.error('Fehler beim Laden der Tarife für Bearbeitung:', error);
tarifSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
}
}
async function loadTarifoptionenForEdit() {
const tarifId = document.getElementById('editTarif').value;
const container = document.getElementById('editTarifoptionenContainer');
if (!tarifId) {
container.innerHTML = '<div>Erst Tarif wählen...</div>';
return;
}
try {
const tarifoptionen = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`);
if (tarifoptionen.length === 0) {
container.innerHTML = '<div>Keine Tarifoptionen verfügbar</div>';
return;
}
container.innerHTML = '';
tarifoptionen.forEach(option => {
const optionDiv = document.createElement('div');
optionDiv.style.marginBottom = '5px';
const label = document.createElement('label');
label.style.fontWeight = 'normal';
label.style.display = 'block';
label.style.cursor = 'pointer';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.name = 'editTarifoptionen';
checkbox.value = option.id;
// Add change event to handle visual feedback
checkbox.addEventListener('change', function() {
if (this.checked) {
label.style.backgroundColor = '#e8f5e8';
label.style.padding = '5px';
label.style.borderRadius = '3px';
label.style.fontWeight = 'bold';
} else {
// Check if this was originally selected
const wasOriginallySelected = window.currentEditTarifoptionen &&
window.currentEditTarifoptionen.some(current => (current.tarifoption_id || current.id) == option.id);
if (!wasOriginallySelected) {
label.style.backgroundColor = '';
label.style.padding = '';
label.style.borderRadius = '';
label.style.fontWeight = 'normal';
} else {
// Keep highlighting but show it's deselected
label.style.backgroundColor = '#ffe8e8';
label.style.padding = '5px';
label.style.borderRadius = '3px';
label.style.fontWeight = 'bold';
}
}
});
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + option.name + ' '));
const priceSpan = document.createElement('span');
priceSpan.style.color = '#666';
priceSpan.textContent = `(${option.provision_euro >= 0 ? '+' : ''}${option.provision_euro}€)`;
label.appendChild(priceSpan);
optionDiv.appendChild(label);
container.appendChild(optionDiv);
});
} catch (error) {
console.error('Fehler beim Laden der Tarifoptionen für Bearbeitung:', error);
container.innerHTML = '<div>Fehler beim Laden der Tarifoptionen</div>';
}
}
function deleteSchaltung(id, schaltungsId, name) {
document.getElementById('deleteSchaltungId').value = id;
document.getElementById('deleteSchaltungInfo').textContent = `${schaltungsId} - ${name}`;
document.getElementById('deleteReason').value = '';
document.getElementById('deleteSchaltungModal').style.display = 'block';
}
async function confirmDeleteSchaltung() {
const id = document.getElementById('deleteSchaltungId').value;
const reason = document.getElementById('deleteReason').value.trim();
if (!reason) {
showMessage('Bitte geben Sie einen Löschgrund ein', 'error');
return;
}
try {
await apiRequest(`/api/schaltungen/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ loeschgrund: reason })
});
closeModal('deleteSchaltungModal');
showMessage('Schaltung erfolgreich gelöscht und archiviert');
await refreshDataAfterSchaltungChange();
} catch (error) {
console.error('Fehler beim Löschen der Schaltung:', error);
}
}
// Load Deleted Schaltungen
async function loadGeloeschteSchaltungen() {
try {
const geloeschte = await apiRequest('/api/geloeschte-schaltungen');
const tbody = document.querySelector('#geloeschteSchaltungenTable tbody');
if (!tbody) return;
tbody.innerHTML = '';
if (!geloeschte.length) {
tbody.innerHTML = '<tr><td colspan="9" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Keine gelöschten Schaltungen</td></tr>';
return;
}
geloeschte.forEach(s => {
const tr = document.createElement('tr');
tr.className = 'border-b border-outline-variant/30 even:bg-surface-container-low/50 hover:bg-surface-variant/30 transition-colors';
tr.innerHTML = `
<td class="px-md py-xs font-label-numeric text-label-numeric text-right text-on-surface-variant">${escapeHtml(s.schaltungs_id)}</td>
<td class="px-md py-xs">
<div class="flex flex-col gap-0.5">
<span class="font-label-numeric text-[11px] text-on-surface-variant">E: ${escapeHtml(formatDate(s.eingabe_datum))}</span>
<span class="font-label-numeric text-[11px] text-primary">A: ${escapeHtml(formatDate(s.schaltungs_datum))}</span>
</div>
</td>
<td class="px-md py-xs">
<div class="font-semibold text-on-background">${escapeHtml((s.vorname || '') + ' ' + (s.nachname || ''))}</div>
<div class="text-[12px] text-on-surface-variant">${escapeHtml(s.firma || '–')}</div>
</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-on-surface-variant">${escapeHtml(s.rufnummer || '–')}</td>
<td class="px-md py-xs">
<span class="bg-primary/10 text-primary border border-primary/20 px-2 py-0.5 rounded-full text-label-caps">${escapeHtml(s.anbieter_name || '')}</span>
</td>
<td class="px-md py-xs">${escapeHtml(s.tarif_name || '')}</td>
<td class="px-md py-xs text-on-surface-variant">${escapeHtml(s.hardware || '–')}</td>
<td class="px-md py-xs">
<span class="bg-error/10 text-error border border-error/20 px-2 py-0.5 rounded-full text-label-caps">${escapeHtml(s.loeschgrund || '')}</span>
</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-on-surface-variant">${escapeHtml(formatDateTime(s.geloescht_am))}</td>
`;
tbody.appendChild(tr);
});
} catch (error) {
console.error('Fehler beim Laden der gelöschten Schaltungen:', error);
}
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '';
const d = new Date(dateTimeString);
if (isNaN(d.getTime())) return String(dateTimeString);
const dd = String(d.getDate()).padStart(2, '0');
const mm = String(d.getMonth() + 1).padStart(2, '0');
const yyyy = d.getFullYear();
const HH = String(d.getHours()).padStart(2, '0');
const MM = String(d.getMinutes()).padStart(2, '0');
return `${dd}.${mm}.${yyyy} ${HH}:${MM}`;
}
// Filter functionality
let allSchaltungen = [];
let allProvisionen = [];
// Modified loadSchaltungen to store data and setup filters
const originalLoadSchaltungen = loadSchaltungen;
loadSchaltungen = async function() {
try {
const data = await apiRequest('/api/schaltungen');
allSchaltungen = data;
// Update filter dropdowns
await updateFilterDropdowns();
// Apply current filters
applyFiltersOverview();
} catch (error) {
console.error('Fehler beim Laden der Schaltungen:', error);
}
};
// Update filter dropdowns from administration data
async function updateFilterDropdowns() {
try {
// Get all anbieter from administration
const anbieter = await apiRequest('/api/anbieter');
// Update Overview anbieter filter
const anbieterOverview = document.getElementById('filterAnbieterOverview');
if (anbieterOverview) {
anbieterOverview.innerHTML = '<option value="">Alle Anbieter</option>';
anbieter.forEach(a => {
anbieterOverview.innerHTML += `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`;
});
}
// Update Provisionen anbieter filter
const anbieterProvisionen = document.getElementById('filterAnbieterProvisionen');
if (anbieterProvisionen) {
anbieterProvisionen.innerHTML = '<option value="">Alle Anbieter</option>';
anbieter.forEach(a => {
anbieterProvisionen.innerHTML += `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`;
});
}
// Get all tarife from all anbieter
const tarifOverview = document.getElementById('filterTarifOverview');
const tarifProvisionen = document.getElementById('filterTarifProvisionen');
if (tarifOverview) {
tarifOverview.innerHTML = '<option value="">Alle Tarife</option>';
}
if (tarifProvisionen) {
tarifProvisionen.innerHTML = '<option value="">Alle Tarife</option>';
}
// Load tarife for each anbieter
for (const a of anbieter) {
try {
const tarife = await apiRequest(`/api/tarife/${a.id}`);
tarife.forEach(t => {
const optionText = `${a.name} - ${t.name}`;
const optionValue = t.name;
if (tarifOverview) {
tarifOverview.innerHTML += `<option value="${escapeHtml(optionValue)}">${escapeHtml(optionText)}</option>`;
}
if (tarifProvisionen) {
tarifProvisionen.innerHTML += `<option value="${escapeHtml(optionValue)}">${escapeHtml(optionText)}</option>`;
}
});
} catch (error) {
console.warn(`Fehler beim Laden der Tarife für Anbieter ${a.name}:`, error);
}
}
} catch (error) {
console.error('Fehler beim Laden der Filter-Daten:', error);
}
}
// Apply filters for Overview tab
function applyFiltersOverview() {
const anbieterFilter = document.getElementById('filterAnbieterOverview')?.value || '';
const tarifFilter = document.getElementById('filterTarifOverview')?.value || '';
const schaltungsIdFilter = document.getElementById('filterSchaltungsIdOverview')?.value || '';
const rufnummerFilter = document.getElementById('filterRufnummerOverview')?.value || '';
const datumVon = document.getElementById('filterDatumVonOverview')?.value || '';
const datumBis = document.getElementById('filterDatumBisOverview')?.value || '';
let filteredData = allSchaltungen.filter(schaltung => {
// Anbieter filter
if (anbieterFilter && schaltung.anbieter_name !== anbieterFilter) {
return false;
}
// Tarif filter
if (tarifFilter && schaltung.tarif_name !== tarifFilter) {
return false;
}
// Schaltungs-ID filter (supports multiple comma-separated IDs)
if (schaltungsIdFilter) {
const schaltungsIds = schaltungsIdFilter.split(',').map(id => id.trim().toLowerCase()).filter(id => id.length > 0);
if (schaltungsIds.length > 0) {
const schaltungsIdLower = schaltung.schaltungs_id.toLowerCase();
const matches = schaltungsIds.some(searchId => schaltungsIdLower.includes(searchId));
if (!matches) {
return false;
}
}
}
// Rufnummer filter (case insensitive partial match)
if (rufnummerFilter && !schaltung.rufnummer.toLowerCase().includes(rufnummerFilter.toLowerCase())) {
return false;
}
// Date range filter
const schaltungsDatum = new Date(schaltung.schaltungs_datum);
if (datumVon && schaltungsDatum < new Date(datumVon)) {
return false;
}
if (datumBis && schaltungsDatum > new Date(datumBis)) {
return false;
}
return true;
});
// Update table with filtered data
updateSchaltungenTable(filteredData);
}
// Apply filters for Provisionen tab
function applyFiltersProvisionen() {
const anbieterFilter = document.getElementById('filterAnbieterProvisionen')?.value || '';
const tarifFilter = document.getElementById('filterTarifProvisionen')?.value || '';
const schaltungsIdFilter = document.getElementById('filterSchaltungsIdProvisionen')?.value || '';
const rufnummerFilter = document.getElementById('filterRufnummerProvisionen')?.value || '';
const datumVon = document.getElementById('filterDatumVonProvisionen')?.value || '';
const datumBis = document.getElementById('filterDatumBisProvisionen')?.value || '';
let filteredSchaltungen = allProvisionen.schaltungen.filter(schaltung => {
// Anbieter filter
if (anbieterFilter && schaltung.anbieter_name !== anbieterFilter) {
return false;
}
// Tarif filter
if (tarifFilter && schaltung.tarif_name !== tarifFilter) {
return false;
}
// Schaltungs-ID filter (supports multiple comma-separated IDs)
if (schaltungsIdFilter) {
const schaltungsIds = schaltungsIdFilter.split(',').map(id => id.trim().toLowerCase()).filter(id => id.length > 0);
if (schaltungsIds.length > 0) {
const schaltungsIdLower = schaltung.schaltungs_id.toLowerCase();
const matches = schaltungsIds.some(searchId => schaltungsIdLower.includes(searchId));
if (!matches) {
return false;
}
}
}
// Rufnummer filter (case insensitive partial match)
if (rufnummerFilter && !schaltung.rufnummer.toLowerCase().includes(rufnummerFilter.toLowerCase())) {
return false;
}
// Date range filter
const schaltungsDatum = new Date(schaltung.schaltungs_datum);
if (datumVon && schaltungsDatum < new Date(datumVon)) {
return false;
}
if (datumBis && schaltungsDatum > new Date(datumBis)) {
return false;
}
return true;
});
// Recalculate totals for filtered data
const totalShopProvision = filteredSchaltungen.reduce((sum, row) => {
return sum + parseFloat(row.berechnete_shop_provision || 0);
}, 0);
const totalMitarbeiterProvision = filteredSchaltungen.reduce((sum, row) => {
return sum + parseFloat(row.berechnete_mitarbeiter_provision || 0);
}, 0);
// Update table and totals with filtered data
updateProvisionenTable(filteredSchaltungen, totalShopProvision, totalMitarbeiterProvision);
}
// Update Schaltungen table with filtered data
function updateSchaltungenTable(schaltungen) {
const tbody = document.querySelector('#schaltungenTable tbody');
if (!tbody) return;
if (!schaltungen || schaltungen.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Keine Schaltungen entsprechen den Filterkriterien</td></tr>';
return;
}
// Delegate to the canonical renderer used by loadSchaltungen.
renderSchaltungenRows(schaltungen);
}
// Format a number as German currency: 1234.5 -> "1.234,50 €"
function formatEuro(value) {
const n = Number(value) || 0;
return n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
}
// Update Provisionen table with filtered data (Stitch style).
function updateProvisionenTable(schaltungen, totalShopProvision, totalMitarbeiterProvision) {
const tbody = document.querySelector('#provisionenTable tbody');
const totalShopSpan = document.getElementById('totalShopProvision');
const totalMitarbeiterSpan = document.getElementById('totalMitarbeiterProvision');
if (!tbody) return;
tbody.innerHTML = '';
if (!schaltungen || schaltungen.length === 0) {
const tr = document.createElement('tr');
tr.innerHTML = '<td colspan="8" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Keine Provisionen entsprechen den Filterkriterien</td>';
tbody.appendChild(tr);
if (totalShopSpan) totalShopSpan.textContent = formatEuro(0);
if (totalMitarbeiterSpan) totalMitarbeiterSpan.textContent = formatEuro(0);
return;
}
schaltungen.forEach(s => {
const shop = Number(s.berechnete_shop_provision) || 0;
const ma = Number(s.berechnete_mitarbeiter_provision) || 0;
const quelle = s.shop_quelle || s.quelle || '';
const tr = document.createElement('tr');
tr.className = 'border-b border-outline-variant/30 even:bg-surface-container-low/50 hover:bg-surface-variant/30 transition-colors';
tr.innerHTML = `
<td class="px-md py-xs font-label-numeric text-label-numeric text-right text-on-surface-variant">${escapeHtml(s.schaltungs_id)}</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-on-surface">${escapeHtml(formatDate(s.schaltungs_datum))}</td>
<td class="px-md py-xs">
<div class="font-semibold text-on-background">${escapeHtml((s.vorname || '') + ' ' + (s.nachname || ''))}</div>
<div class="text-[12px] text-on-surface-variant">${escapeHtml(s.firma || '–')}</div>
</td>
<td class="px-md py-xs">
<span class="bg-primary/10 text-primary border border-primary/20 px-2 py-0.5 rounded-full text-label-caps">${escapeHtml(s.anbieter_name || '')}</span>
</td>
<td class="px-md py-xs">${escapeHtml(s.tarif_name || '')}</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-right text-on-surface font-semibold">${escapeHtml(formatEuro(shop))}</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-right text-on-surface font-semibold">${escapeHtml(formatEuro(ma))}</td>
<td class="px-md py-xs text-center">
<span class="material-symbols-outlined text-outline hover:text-primary transition-colors text-[18px] cursor-help" title="${escapeHtml(quelle)}">info</span>
</td>
`;
tbody.appendChild(tr);
});
if (totalShopSpan) totalShopSpan.textContent = formatEuro(totalShopProvision);
if (totalMitarbeiterSpan) totalMitarbeiterSpan.textContent = formatEuro(totalMitarbeiterProvision);
}
// Clear filters functions
function clearFiltersOverview() {
document.getElementById('filterAnbieterOverview').value = '';
document.getElementById('filterTarifOverview').value = '';
document.getElementById('filterSchaltungsIdOverview').value = '';
document.getElementById('filterRufnummerOverview').value = '';
document.getElementById('filterDatumVonOverview').value = '';
document.getElementById('filterDatumBisOverview').value = '';
applyFiltersOverview();
}
function clearFiltersProvisionen() {
document.getElementById('filterAnbieterProvisionen').value = '';
document.getElementById('filterTarifProvisionen').value = '';
document.getElementById('filterSchaltungsIdProvisionen').value = '';
document.getElementById('filterRufnummerProvisionen').value = '';
document.getElementById('filterDatumVonProvisionen').value = '';
document.getElementById('filterDatumBisProvisionen').value = '';
applyFiltersProvisionen();
}
// Add event listeners for filter changes
document.addEventListener('DOMContentLoaded', function() {
// Overview filters
const overviewFilters = [
'filterAnbieterOverview',
'filterTarifOverview',
'filterSchaltungsIdOverview',
'filterRufnummerOverview',
'filterDatumVonOverview',
'filterDatumBisOverview'
];
overviewFilters.forEach(filterId => {
const element = document.getElementById(filterId);
if (element) {
if (element.type === 'text') {
element.addEventListener('input', applyFiltersOverview);
} else {
element.addEventListener('change', applyFiltersOverview);
}
}
});
// Provisionen filters
const provisionenFilters = [
'filterAnbieterProvisionen',
'filterTarifProvisionen',
'filterSchaltungsIdProvisionen',
'filterRufnummerProvisionen',
'filterDatumVonProvisionen',
'filterDatumBisProvisionen'
];
provisionenFilters.forEach(filterId => {
const element = document.getElementById(filterId);
if (element) {
if (element.type === 'text') {
element.addEventListener('input', applyFiltersProvisionen);
} else {
element.addEventListener('change', applyFiltersProvisionen);
}
}
});
});
// Tarifoptionen Administration Functions
async function loadTarifoptionenAdmin() {
await loadAnbieterForTarifoptionen();
await displayTarifoptionenAdmin();
}
async function loadAnbieterForTarifoptionen() {
try {
const anbieter = await apiRequest('/api/anbieter');
const select = document.getElementById('tarifoptionAnbieter');
select.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter:', error);
}
}
async function loadTarifeForOptions() {
const anbieterId = document.getElementById('tarifoptionAnbieter').value;
const container = document.getElementById('tarifCheckboxContainer');
if (!anbieterId) {
container.innerHTML = '<div style="color: #666; font-style: italic;">Erst Anbieter wählen...</div>';
return;
}
try {
const tarife = await apiRequest(`/api/tarife/${anbieterId}`);
if (tarife.length === 0) {
container.innerHTML = '<div style="color: #666; font-style: italic;">Keine Tarife für diesen Anbieter gefunden.</div>';
return;
}
let checkboxHtml = '';
tarife.forEach(tarif => {
checkboxHtml += `
<div style="margin-bottom: 8px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" value="${tarif.id}" class="tarif-checkbox" style="margin: 0;">
<span>${escapeHtml(tarif.name)}</span>
</label>
</div>
`;
});
container.innerHTML = checkboxHtml;
} catch (error) {
console.error('Fehler beim Laden der Tarife:', error);
container.innerHTML = '<div style="color: #d32f2f;">Fehler beim Laden der Tarife</div>';
}
}
// Toggle tariff selection based on anbieter-wide checkbox
function toggleTarifSelection() {
const checkbox = document.getElementById('tarifoptionAnbieterWeit');
const tarifSelectionContainer = document.getElementById('tarifSelectionContainer');
const anbieterSelect = document.getElementById('tarifoptionAnbieter');
if (checkbox.checked) {
// Anbieter-wide option - hide tarif selection
tarifSelectionContainer.style.display = 'none';
} else {
// Tariff-specific option - show tarif selection
tarifSelectionContainer.style.display = 'block';
if (anbieterSelect.value) {
loadTarifeForOptions();
} else {
document.getElementById('tarifCheckboxContainer').innerHTML =
'<div style="color: #666; font-style: italic;">Erst Anbieter wählen...</div>';
}
}
}
// Select/Deselect all tarifs functions
function selectAllTarifs() {
const checkboxes = document.querySelectorAll('.tarif-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
}
function deselectAllTarifs() {
const checkboxes = document.querySelectorAll('.tarif-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
}
// Edit Tarifoption functions
function toggleEditTarifSelection() {
const checkbox = document.getElementById('editTarifoptionAnbieterWeit');
const tarifSelectionContainer = document.getElementById('editTarifSelectionContainer');
const anbieterSelect = document.getElementById('editTarifoptionAnbieter');
if (checkbox.checked) {
// Anbieter-wide option - hide tarif selection
tarifSelectionContainer.style.display = 'none';
} else {
// Tariff-specific option - show tarif selection
tarifSelectionContainer.style.display = 'block';
if (anbieterSelect.value) {
loadTarifeForEditOptions();
} else {
document.getElementById('editTarifCheckboxContainer').innerHTML =
'<div style="color: #666; font-style: italic;">Erst Anbieter wählen...</div>';
}
}
}
async function loadTarifeForEditOptions() {
const anbieterId = document.getElementById('editTarifoptionAnbieter').value;
const container = document.getElementById('editTarifCheckboxContainer');
const counterElement = document.getElementById('editTarifCounter');
if (!anbieterId) {
container.innerHTML = '<div style="padding: 40px; text-align: center; color: #6c757d; font-style: italic;">Erst Anbieter wählen...</div>';
counterElement.textContent = '';
return;
}
try {
const tarife = await apiRequest(`/api/tarife/${anbieterId}`);
if (tarife.length === 0) {
container.innerHTML = '<div style="padding: 40px; text-align: center; color: #6c757d; font-style: italic;">Keine Tarife für diesen Anbieter gefunden.</div>';
counterElement.textContent = '';
return;
}
let checkboxHtml = '<div style="padding: 15px;">';
tarife.forEach((tarif, index) => {
checkboxHtml += `
<div class="edit-tarif-item" style="margin-bottom: 12px; padding: 12px; border-radius: 6px; background: #f8f9fa; border: 1px solid #e9ecef; transition: all 0.2s;" data-tarif-name="${escapeHtml(tarif.name.toLowerCase())}">
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; margin: 0;">
<input type="checkbox" value="${tarif.id}" class="edit-tarif-checkbox" style="width: 16px; height: 16px; cursor: pointer; accent-color: #007bff;">
<div style="flex: 1;">
<div style="font-weight: 500; color: #333; margin-bottom: 2px;">${escapeHtml(tarif.name)}</div>
${tarif.beschreibung ? `<div style="font-size: 12px; color: #6c757d;">${escapeHtml(tarif.beschreibung)}</div>` : ''}
${tarif.provision_euro ? `<div style="font-size: 12px; color: #28a745; font-weight: 500;">Provision: ${escapeHtml(String(tarif.provision_euro))}€</div>` : ''}
</div>
</label>
</div>
`;
});
checkboxHtml += '</div>';
container.innerHTML = checkboxHtml;
updateEditTarifCounter();
// Add hover effects
container.querySelectorAll('.edit-tarif-item').forEach(item => {
item.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#e3f2fd';
this.style.borderColor = '#2196f3';
});
item.addEventListener('mouseleave', function() {
this.style.backgroundColor = '#f8f9fa';
this.style.borderColor = '#e9ecef';
});
});
// Add change listeners to checkboxes
container.querySelectorAll('.edit-tarif-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateEditTarifCounter);
});
} catch (error) {
console.error('Fehler beim Laden der Tarife:', error);
container.innerHTML = '<div style="padding: 40px; text-align: center; color: #dc3545; font-style: italic;">Fehler beim Laden der Tarife.</div>';
counterElement.textContent = '';
}
}
function selectAllEditTarifs() {
const checkboxes = document.querySelectorAll('.edit-tarif-checkbox:not([style*="display: none"])');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
updateEditTarifCounter();
}
function deselectAllEditTarifs() {
const checkboxes = document.querySelectorAll('.edit-tarif-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
updateEditTarifCounter();
}
function updateEditTarifCounter() {
const totalCheckboxes = document.querySelectorAll('.edit-tarif-checkbox').length;
const checkedCheckboxes = document.querySelectorAll('.edit-tarif-checkbox:checked').length;
const visibleCheckboxes = document.querySelectorAll('.edit-tarif-checkbox:not([style*="display: none"])').length;
const counterElement = document.getElementById('editTarifCounter');
if (counterElement) {
if (totalCheckboxes === 0) {
counterElement.textContent = '';
} else {
const filterActive = visibleCheckboxes < totalCheckboxes;
counterElement.textContent = `${checkedCheckboxes} von ${totalCheckboxes} Tarifen ausgewählt${filterActive ? ` (${visibleCheckboxes} sichtbar)` : ''}`;
}
}
}
function filterEditTarifs() {
const searchTerm = document.getElementById('editTarifSearch').value.toLowerCase();
const tarifItems = document.querySelectorAll('.edit-tarif-item');
tarifItems.forEach(item => {
const tarifName = item.getAttribute('data-tarif-name');
if (tarifName && tarifName.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
updateEditTarifCounter();
}
// Add Tarifoption Modal Functions
function openAddTarifoptionModal() {
// Load anbieter for the dropdown
loadAnbieterForAddModal();
// Reset form
document.getElementById('addTarifoptionForm').reset();
document.getElementById('addTarifoptionAnbieterWeit').checked = true;
toggleAddTarifSelection();
// Show modal
document.getElementById('addTarifoptionModal').style.display = 'block';
}
async function loadAnbieterForAddModal() {
try {
const anbieter = await apiRequest('/api/anbieter');
const anbieterSelect = document.getElementById('addTarifoptionAnbieter');
anbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
anbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter:', error);
}
}
function toggleAddTarifSelection() {
const checkbox = document.getElementById('addTarifoptionAnbieterWeit');
const tarifSelectionContainer = document.getElementById('addTarifSelectionContainer');
const anbieterWeitInfo = document.getElementById('addAnbieterWeitInfo');
const anbieterSelect = document.getElementById('addTarifoptionAnbieter');
if (checkbox.checked) {
// Anbieter-wide option - hide tarif selection
tarifSelectionContainer.style.display = 'none';
anbieterWeitInfo.style.display = 'block';
} else {
// Tariff-specific option - show tarif selection
tarifSelectionContainer.style.display = 'block';
anbieterWeitInfo.style.display = 'none';
if (anbieterSelect.value) {
loadTarifeForAddOptions();
} else {
document.getElementById('addTarifCheckboxContainer').innerHTML =
'<div style="padding: 40px; text-align: center; color: #6c757d; font-style: italic;">Erst Anbieter wählen...</div>';
}
}
}
async function loadTarifeForAddOptions() {
const anbieterId = document.getElementById('addTarifoptionAnbieter').value;
const container = document.getElementById('addTarifCheckboxContainer');
const counterElement = document.getElementById('addTarifCounter');
if (!anbieterId) {
container.innerHTML = '<div style="padding: 40px; text-align: center; color: #6c757d; font-style: italic;">Erst Anbieter wählen...</div>';
counterElement.textContent = '';
return;
}
try {
const tarife = await apiRequest(`/api/tarife/${anbieterId}`);
if (tarife.length === 0) {
container.innerHTML = '<div style="padding: 40px; text-align: center; color: #6c757d; font-style: italic;">Keine Tarife für diesen Anbieter gefunden.</div>';
counterElement.textContent = '';
return;
}
let checkboxHtml = '<div style="padding: 15px;">';
tarife.forEach((tarif, index) => {
checkboxHtml += `
<div class="add-tarif-item" style="margin-bottom: 12px; padding: 12px; border-radius: 6px; background: #f8f9fa; border: 1px solid #e9ecef; transition: all 0.2s;" data-tarif-name="${escapeHtml(tarif.name.toLowerCase())}">
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; margin: 0;">
<input type="checkbox" value="${tarif.id}" class="add-tarif-checkbox" style="width: 16px; height: 16px; cursor: pointer; accent-color: #28a745;">
<div style="flex: 1;">
<div style="font-weight: 500; color: #333; margin-bottom: 2px;">${escapeHtml(tarif.name)}</div>
${tarif.beschreibung ? `<div style="font-size: 12px; color: #6c757d;">${escapeHtml(tarif.beschreibung)}</div>` : ''}
${tarif.provision_euro ? `<div style="font-size: 12px; color: #28a745; font-weight: 500;">Provision: ${escapeHtml(String(tarif.provision_euro))}€</div>` : ''}
</div>
</label>
</div>
`;
});
checkboxHtml += '</div>';
container.innerHTML = checkboxHtml;
updateAddTarifCounter();
// Add hover effects
container.querySelectorAll('.add-tarif-item').forEach(item => {
item.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#e3f2fd';
this.style.borderColor = '#2196f3';
});
item.addEventListener('mouseleave', function() {
this.style.backgroundColor = '#f8f9fa';
this.style.borderColor = '#e9ecef';
});
});
// Add change listeners to checkboxes
container.querySelectorAll('.add-tarif-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateAddTarifCounter);
});
} catch (error) {
console.error('Fehler beim Laden der Tarife:', error);
container.innerHTML = '<div style="padding: 40px; text-align: center; color: #dc3545; font-style: italic;">Fehler beim Laden der Tarife.</div>';
counterElement.textContent = '';
}
}
function selectAllAddTarifs() {
const checkboxes = document.querySelectorAll('.add-tarif-checkbox:not([style*="display: none"])');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
updateAddTarifCounter();
}
function deselectAllAddTarifs() {
const checkboxes = document.querySelectorAll('.add-tarif-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
updateAddTarifCounter();
}
function updateAddTarifCounter() {
const totalCheckboxes = document.querySelectorAll('.add-tarif-checkbox').length;
const checkedCheckboxes = document.querySelectorAll('.add-tarif-checkbox:checked').length;
const visibleCheckboxes = document.querySelectorAll('.add-tarif-checkbox:not([style*="display: none"])').length;
const counterElement = document.getElementById('addTarifCounter');
if (counterElement) {
if (totalCheckboxes === 0) {
counterElement.textContent = '';
} else {
const filterActive = visibleCheckboxes < totalCheckboxes;
counterElement.textContent = `${checkedCheckboxes} von ${totalCheckboxes} Tarifen ausgewählt${filterActive ? ` (${visibleCheckboxes} sichtbar)` : ''}`;
}
}
}
function filterAddTarifs() {
const searchTerm = document.getElementById('addTarifSearch').value.toLowerCase();
const tarifItems = document.querySelectorAll('.add-tarif-item');
tarifItems.forEach(item => {
const tarifName = item.getAttribute('data-tarif-name');
if (tarifName && tarifName.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
updateAddTarifCounter();
}
// Setup form submission for the new add modal
document.addEventListener('DOMContentLoaded', function() {
const addForm = document.getElementById('addTarifoptionForm');
if (addForm) {
addForm.addEventListener('submit', async function(e) {
e.preventDefault();
const anbieterId = document.getElementById('addTarifoptionAnbieter').value;
const isAnbieterWeit = document.getElementById('addTarifoptionAnbieterWeit').checked;
const name = document.getElementById('addTarifoptionName').value;
const code = document.getElementById('addTarifoptionCode').value;
const provisionEuro = document.getElementById('addTarifoptionProvisionBetrag').value;
const beschreibung = document.getElementById('addTarifoptionBeschreibung').value;
if (!anbieterId || !name) {
showMessage('Bitte Anbieter und Name auswählen', 'error');
return;
}
let tarifIds = [];
if (!isAnbieterWeit) {
// Get selected tarifs from checkboxes
const checkedBoxes = document.querySelectorAll('.add-tarif-checkbox:checked');
tarifIds = Array.from(checkedBoxes).map(cb => parseInt(cb.value));
if (tarifIds.length === 0) {
showMessage('Bitte mindestens einen Tarif auswählen oder Anbieter-weit aktivieren', 'error');
return;
}
}
try {
const data = {
anbieter_id: anbieterId,
tarif_ids: tarifIds,
is_anbieter_weit: isAnbieterWeit,
name: name,
code: code || null,
provision_euro: parseFloat(provisionEuro) || 0,
beschreibung: beschreibung || null
};
const result = await apiRequest('/api/tarifoptionen/multi', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
showMessage(`Tarifoption "${name}" erfolgreich erstellt${result.assignments ? ` (${result.assignments} Tarif-Zuordnungen)` : ''}`, 'success');
// Close modal and refresh admin view
closeModal('addTarifoptionModal');
if (typeof displayTarifoptionenAdmin === 'function') {
await displayTarifoptionenAdmin();
}
} catch (error) {
console.error('Fehler beim Erstellen der Tarifoption:', error);
// Error message is already shown by apiRequest function
}
});
}
});
async function addTarifoption() {
const anbieterId = document.getElementById('tarifoptionAnbieter').value;
const isAnbieterWeit = document.getElementById('tarifoptionAnbieterWeit').checked;
const name = document.getElementById('tarifoptionName').value;
const code = document.getElementById('tarifoptionCode').value;
const provisionEuro = document.getElementById('tarifoptionProvisionBetrag').value;
const beschreibung = document.getElementById('tarifoptionBeschreibung').value;
if (!anbieterId || !name) {
showMessage('Bitte Anbieter und Name auswählen', 'error');
return;
}
let tarifIds = [];
if (!isAnbieterWeit) {
// Get selected tarifs from checkboxes
const checkedBoxes = document.querySelectorAll('.tarif-checkbox:checked');
tarifIds = Array.from(checkedBoxes).map(cb => parseInt(cb.value));
if (tarifIds.length === 0) {
showMessage('Bitte mindestens einen Tarif auswählen oder Anbieter-weit aktivieren', 'error');
return;
}
}
try {
const data = {
anbieter_id: anbieterId,
tarif_ids: tarifIds,
is_anbieter_weit: isAnbieterWeit,
name: name,
code: code || null,
provision_euro: parseFloat(provisionEuro) || 0,
beschreibung: beschreibung || null
};
const result = await apiRequest('/api/tarifoptionen/multi', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
showMessage(`Tarifoption "${name}" erfolgreich erstellt (${result.assignments} Tarif-Zuordnungen)`, 'success');
// Reset form
document.getElementById('tarifoptionAnbieter').value = '';
document.getElementById('tarifoptionName').value = '';
document.getElementById('tarifoptionCode').value = '';
document.getElementById('tarifoptionProvisionBetrag').value = '';
document.getElementById('tarifoptionBeschreibung').value = '';
document.getElementById('tarifoptionAnbieterWeit').checked = true;
toggleTarifSelection();
// Reload tarifoptionen list
displayTarifoptionenAdmin();
} catch (error) {
console.error('Fehler beim Hinzufügen der Tarifoption:', error);
showMessage('Fehler beim Hinzufügen der Tarifoption: ' + error.message, 'error');
}
}
async function displayTarifoptionenAdmin() {
try {
// Try to get new format first, fallback to old format
let optionen;
try {
optionen = await apiRequest('/api/tarifoptionen/multi');
} catch (error) {
console.log('New API not available, falling back to old format');
optionen = await apiRequest('/api/tarifoptionen');
}
const tbody = document.getElementById('tarifoptionenAdminList');
tbody.innerHTML = '';
if (optionen.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; color: #666;">Keine Tarifoptionen vorhanden</td></tr>';
return;
}
optionen.forEach(option => {
const provision = `${(option.provision_euro || 0).toFixed(2).replace('.', ',')}€`;
// tarifDisplayHtml is the rendered HTML; tarifPlain is the plain-text variant for the title attr.
let tarifDisplayHtml;
let tarifPlain;
if (option.tarif_names) {
tarifPlain = option.tarif_names || 'Alle Tarife';
tarifDisplayHtml = option.tarif_names
? escapeHtml(option.tarif_names)
: '<em style="color: #28a745;">Alle Tarife</em>';
} else {
if (option.tarif_id) {
tarifPlain = option.tarif_name || 'Unbekannter Tarif';
tarifDisplayHtml = escapeHtml(tarifPlain);
} else {
tarifPlain = 'Alle Tarife';
tarifDisplayHtml = '<em style="color: #28a745;">Alle Tarife</em>';
}
}
const anbieterName = option.anbieter_name || 'Mehrere Anbieter';
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${option.id}</td>
<td>${escapeHtml(anbieterName)}</td>
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(tarifPlain)}">${tarifDisplayHtml}</td>
<td>${escapeHtml(option.name)}</td>
<td>${escapeHtml(option.code || '-')}</td>
<td>${escapeHtml(provision)}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(option.beschreibung || '-')}</td>
<td>
<button data-to-action="edit" data-id="${option.id}" class="btn-success" style="margin-right: 5px;">Bearbeiten</button>
<button data-to-action="delete" data-id="${option.id}" class="btn-danger">Löschen</button>
</td>
`;
tr.querySelectorAll('button[data-to-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.toAction;
const id = parseInt(btn.dataset.id, 10);
if (action === 'edit') editTarifoption(id);
else if (action === 'delete') deleteTarifoption(id);
});
});
tbody.appendChild(tr);
});
} catch (error) {
console.error('Fehler beim Laden der Tarifoptionen:', error);
const tbody = document.getElementById('tarifoptionenAdminList');
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: #666;">Fehler beim Laden der Daten</td></tr>';
}
// Update search counter after loading data
updateTarifoptionSearchCounter();
}
// Tarifoptionen search functions
function filterTarifoptionenAdmin() {
const searchTerm = document.getElementById('tarifoptionSearch').value.toLowerCase();
const tbody = document.getElementById('tarifoptionenAdminList');
const rows = tbody.getElementsByTagName('tr');
let visibleCount = 0;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (row.cells.length < 2) continue; // Skip header or empty rows
// Get text content from relevant columns (Anbieter, Tarif, Option, Code, Beschreibung)
const anbieterText = (row.cells[1]?.textContent || '').toLowerCase();
const tarifText = (row.cells[2]?.textContent || '').toLowerCase();
const optionText = (row.cells[3]?.textContent || '').toLowerCase();
const codeText = (row.cells[4]?.textContent || '').toLowerCase();
const beschreibungText = (row.cells[6]?.textContent || '').toLowerCase();
// Check if search term matches any of the relevant columns
const matches = anbieterText.includes(searchTerm) ||
tarifText.includes(searchTerm) ||
optionText.includes(searchTerm) ||
codeText.includes(searchTerm) ||
beschreibungText.includes(searchTerm);
if (matches || searchTerm === '') {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
}
updateTarifoptionSearchCounter();
}
function clearTarifoptionSearch() {
document.getElementById('tarifoptionSearch').value = '';
filterTarifoptionenAdmin();
}
function updateTarifoptionSearchCounter() {
const tbody = document.getElementById('tarifoptionenAdminList');
const counterElement = document.getElementById('tarifoptionSearchCounter');
const rows = tbody.getElementsByTagName('tr');
let totalCount = 0;
let visibleCount = 0;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (row.cells.length < 2) continue; // Skip header or empty rows
totalCount++;
if (row.style.display !== 'none') {
visibleCount++;
}
}
if (counterElement) {
if (totalCount === 0) {
counterElement.textContent = '';
} else {
const searchTerm = document.getElementById('tarifoptionSearch').value;
if (searchTerm && visibleCount < totalCount) {
counterElement.textContent = `${visibleCount} von ${totalCount} Optionen`;
} else {
counterElement.textContent = `${totalCount} Optionen`;
}
}
}
}
async function editTarifoption(optionId) {
try {
// Load anbieter for the dropdown
const anbieter = await apiRequest('/api/anbieter');
const anbieterSelect = document.getElementById('editTarifoptionAnbieter');
anbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
anbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
// Try to get from new API first, fallback to old API
let optionen;
let option;
try {
optionen = await apiRequest('/api/tarifoptionen/multi');
option = optionen.find(opt => opt.id == optionId);
} catch (error) {
console.log('New API not available, trying old API');
optionen = await apiRequest('/api/tarifoptionen');
option = optionen.find(opt => opt.id == optionId);
}
if (!option) {
showMessage('Tarifoption nicht gefunden', 'error');
return;
}
// Fill modal with current data
document.getElementById('editTarifoptionId').value = option.id;
document.getElementById('editTarifoptionName').value = option.name || '';
document.getElementById('editTarifoptionCode').value = option.code || '';
document.getElementById('editTarifoptionProvisionBetrag').value = option.provision_euro || option.provision_betrag || '';
document.getElementById('editTarifoptionBeschreibung').value = option.beschreibung || '';
// Parse tarif assignments and set anbieter
if (option.tarif_ids && option.tarif_ids.length > 0) {
// For multi-tarif options, we need to find the anbieter
const tarifIds = option.tarif_ids.split(',').map(id => parseInt(id.trim()));
if (tarifIds.length > 0) {
// Get the first tarif to determine anbieter
try {
const tarif = await apiRequest(`/api/tarif/${tarifIds[0]}`);
if (tarif && tarif.anbieter_id) {
anbieterSelect.value = tarif.anbieter_id;
// Check if all tarifs belong to the same anbieter
let allTarifs = [];
for (let tarifId of tarifIds) {
try {
const t = await apiRequest(`/api/tarif/${tarifId}`);
if (t) allTarifs.push(t);
} catch (e) {}
}
const allSameAnbieter = allTarifs.every(t => t.anbieter_id === tarif.anbieter_id);
const anbieterTarifs = await apiRequest(`/api/tarife/${tarif.anbieter_id}`);
const isAllTarifsOfAnbieter = anbieterTarifs.length === tarifIds.length &&
anbieterTarifs.every(at => tarifIds.includes(at.id));
if (allSameAnbieter && isAllTarifsOfAnbieter) {
// This is an anbieter-wide option
document.getElementById('editTarifoptionAnbieterWeit').checked = true;
document.getElementById('editTarifSelectionContainer').style.display = 'none';
} else {
// This is tarif-specific
document.getElementById('editTarifoptionAnbieterWeit').checked = false;
document.getElementById('editTarifSelectionContainer').style.display = 'block';
// Load and select specific tarifs
await loadTarifeForEditOptions();
// Select the specific tarifs
setTimeout(() => {
tarifIds.forEach(tarifId => {
const checkbox = document.querySelector(`.edit-tarif-checkbox[value="${tarifId}"]`);
if (checkbox) checkbox.checked = true;
});
}, 100);
}
}
} catch (error) {
console.error('Error loading tarif details:', error);
}
}
} else {
// Default to anbieter-wide if no specific tarifs
document.getElementById('editTarifoptionAnbieterWeit').checked = true;
document.getElementById('editTarifSelectionContainer').style.display = 'none';
}
// Show modal
document.getElementById('editTarifoptionModal').style.display = 'block';
// Setup form submission
document.getElementById('editTarifoptionForm').onsubmit = async (e) => {
e.preventDefault();
const anbieterId = document.getElementById('editTarifoptionAnbieter').value;
const isAnbieterWeit = document.getElementById('editTarifoptionAnbieterWeit').checked;
if (!anbieterId) {
showMessage('Bitte wählen Sie einen Anbieter aus', 'error');
return;
}
let tarifIds = [];
if (!isAnbieterWeit) {
const checkedBoxes = document.querySelectorAll('.edit-tarif-checkbox:checked');
tarifIds = Array.from(checkedBoxes).map(cb => parseInt(cb.value));
if (tarifIds.length === 0) {
showMessage('Bitte wählen Sie mindestens einen Tarif aus oder aktivieren Sie "Anbieter-weit"', 'error');
return;
}
}
const formData = {
name: document.getElementById('editTarifoptionName').value.trim(),
code: document.getElementById('editTarifoptionCode').value.trim() || null,
provision_euro: document.getElementById('editTarifoptionProvisionBetrag').value ?
parseFloat(document.getElementById('editTarifoptionProvisionBetrag').value) : 0,
beschreibung: document.getElementById('editTarifoptionBeschreibung').value.trim() || null,
anbieter_id: parseInt(anbieterId),
is_anbieter_weit: isAnbieterWeit,
tarif_ids: tarifIds
};
try {
// Try new API first, fallback to old API
try {
await apiRequest(`/api/tarifoptionen/multi/${optionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
} catch (error) {
console.log('New API not available, trying old API');
await apiRequest(`/api/tarifoptionen/${optionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.name,
provision_euro: formData.provision_euro
})
});
}
closeModal('editTarifoptionModal');
showMessage('Tarifoption erfolgreich aktualisiert');
await displayTarifoptionenAdmin();
} catch (error) {
console.error('Fehler beim Aktualisieren der Tarifoption:', error);
// Error message is already shown by apiRequest function
}
};
} catch (error) {
console.error('Fehler beim Laden der Tarifoption-Daten:', error);
}
}
async function deleteTarifoption(optionId) {
if (!confirm('Möchten Sie diese Tarifoption wirklich löschen? Dies entfernt auch alle Zuordnungen zu Tarifen.')) {
return;
}
try {
// Try new API first, fallback to old API
let success = false;
try {
await apiRequest(`/api/tarifoptionen/multi/${optionId}`, {
method: 'DELETE'
});
success = true;
} catch (error) {
console.log('New API not available, trying old API');
await apiRequest(`/api/tarifoptionen/${optionId}`, {
method: 'DELETE'
});
success = true;
}
if (success) {
await displayTarifoptionenAdmin();
showMessage('Tarifoption erfolgreich gelöscht', 'success');
}
} catch (error) {
console.error('Fehler beim Löschen der Tarifoption:', error);
showMessage('Fehler beim Löschen der Tarifoption: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
// Mitarbeiter-Prozentsätze Functions
async function loadMitarbeiterProzentsaetzeAdmin() {
try {
const anbieter = await apiRequest('/api/anbieter');
const container = document.getElementById('mitarbeiterProzentsaetzeList');
container.innerHTML = '';
for (const a of anbieter) {
const prozentsaetze = await apiRequest(`/api/anbieter/${a.id}/mitarbeiter-prozentsaetze`);
const providerDiv = document.createElement('div');
providerDiv.className = 'admin-card';
providerDiv.innerHTML = `
<div class="admin-card-header">
<h4>${escapeHtml(a.name)}</h4>
</div>
<div class="admin-card-content">
<div class="prozentsaetze-list" id="prozentsaetze-${a.id}"></div>
</div>
`;
container.appendChild(providerDiv);
const prozentsaetzeList = document.getElementById(`prozentsaetze-${a.id}`);
if (prozentsaetze.length === 0) {
prozentsaetzeList.innerHTML = '<p class="text-muted">Keine Mitarbeiter-Prozentsätze definiert</p>';
} else {
prozentsaetze.forEach(p => {
const item = document.createElement('div');
item.className = 'list-item';
item.innerHTML = `
<div>
<strong>${escapeHtml(String(p.prozentsatz))}%</strong>
${p.beschreibung ? `- ${escapeHtml(p.beschreibung)}` : ''}
<br><small class="text-muted">Gültig ab: ${escapeHtml(formatDate(p.gueltig_ab))}</small>
</div>
<div>
<button data-mp-action="delete" data-id="${p.id}" class="btn-danger">
Löschen
</button>
</div>
`;
item.querySelector('button[data-mp-action]').addEventListener('click', (e) => {
deleteMitarbeiterProzentsatz(parseInt(e.currentTarget.dataset.id, 10));
});
prozentsaetzeList.appendChild(item);
});
}
}
} catch (error) {
console.error('Fehler beim Laden der Mitarbeiter-Prozentsätze:', error);
}
}
async function loadMitarbeiterAnbieter() {
try {
const anbieter = await apiRequest('/api/anbieter');
const select = document.getElementById('mitarbeiterAnbieter');
select.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (error) {
console.error('Fehler beim Laden der Anbieter:', error);
}
}
async function addMitarbeiterProzentsatz() {
const anbieterId = document.getElementById('mitarbeiterAnbieter').value;
const prozentsatz = document.getElementById('mitarbeiterProzentsatz').value;
const gueltigAb = document.getElementById('mitarbeiterGueltigAb').value;
const beschreibung = document.getElementById('mitarbeiterBeschreibung').value.trim();
if (!anbieterId || !prozentsatz) {
showMessage('Bitte Anbieter und Prozentsatz auswählen', 'error');
return;
}
const prozentsatzNum = parseFloat(prozentsatz);
if (prozentsatzNum < 0 || prozentsatzNum > 100) {
showMessage('Prozentsatz muss zwischen 0 und 100 sein', 'error');
return;
}
try {
const data = {
anbieter_id: anbieterId,
prozentsatz: prozentsatzNum,
gueltig_ab: gueltigAb || new Date().toISOString().split('T')[0],
beschreibung: beschreibung || null
};
await apiRequest('/api/mitarbeiter-prozentsaetze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
// Reset form
document.getElementById('mitarbeiterAnbieter').value = '';
document.getElementById('mitarbeiterProzentsatz').value = '';
document.getElementById('mitarbeiterGueltigAb').value = '';
document.getElementById('mitarbeiterBeschreibung').value = '';
await loadMitarbeiterProzentsaetzeAdmin();
showMessage('Mitarbeiter-Prozentsatz erfolgreich hinzugefügt');
} catch (error) {
console.error('Fehler beim Hinzufügen des Mitarbeiter-Prozentsatzes:', error);
showMessage('Fehler beim Hinzufügen des Mitarbeiter-Prozentsatzes', 'error');
}
}
async function deleteMitarbeiterProzentsatz(id) {
if (!confirm('Möchten Sie diesen Mitarbeiter-Prozentsatz wirklich löschen?')) {
return;
}
try {
await apiRequest(`/api/mitarbeiter-prozentsaetze/${id}`, {
method: 'DELETE'
});
await loadMitarbeiterProzentsaetzeAdmin();
showMessage('Mitarbeiter-Prozentsatz erfolgreich gelöscht');
} catch (error) {
console.error('Fehler beim Löschen des Mitarbeiter-Prozentsatzes:', error);
showMessage('Fehler beim Löschen des Mitarbeiter-Prozentsatzes', 'error');
}
}
// Tarif-Provisionen Functions
async function loadTarifProvisionsAdmin() {
try {
const tarife = await apiRequest('/api/tarife');
const container = document.getElementById('tarifProvisionsList');
container.innerHTML = '';
for (const tarif of tarife) {
const tarifDiv = document.createElement('div');
tarifDiv.className = 'admin-card';
tarifDiv.innerHTML = `
<div class="admin-card-header">
<h4>${escapeHtml(tarif.name)} (${escapeHtml(tarif.anbieter_name)})</h4>
<span class="text-muted">Basis-Provision: ${escapeHtml(String(tarif.provision_euro))}€</span>
</div>
<div class="admin-card-content">
<div class="tarif-provision-controls">
<div class="form-row">
<label>Mitarbeiter-Provision (€):</label>
<input type="number"
id="provision-${tarif.id}"
placeholder="0.00"
step="0.01"
min="0"
value="${escapeHtml(String(tarif.mitarbeiter_provision || ''))}"
style="width: 120px;">
<button data-tp-action="save" data-id="${tarif.id}" class="btn-success">
Speichern
</button>
</div>
<small class="text-muted">
Feste Mitarbeiter-Provision für diesen Tarif
</small>
</div>
</div>
`;
tarifDiv.querySelector('button[data-tp-action]').addEventListener('click', (e) => {
saveTarifProvision(parseInt(e.currentTarget.dataset.id, 10));
});
container.appendChild(tarifDiv);
}
} catch (error) {
console.error('Fehler beim Laden der Tarif-Provisionen:', error);
}
}
async function saveTarifProvision(tarifId) {
const provisionInput = document.getElementById(`provision-${tarifId}`);
const provision = parseFloat(provisionInput.value) || 0;
try {
await apiRequest(`/api/tarife/${tarifId}/mitarbeiter-provision`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mitarbeiter_provision: provision })
});
showMessage('Mitarbeiter-Provision erfolgreich gespeichert');
} catch (error) {
console.error('Fehler beim Speichern der Mitarbeiter-Provision:', error);
showMessage('Fehler beim Speichern der Mitarbeiter-Provision', 'error');
}
}
// PDF Viewer Functions
let currentPdf = null;
let currentPage = 1;
let totalPages = 0;
let currentScale = 1.0;
const scaleStep = 0.2;
// Configure PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
async function viewPdfDocument(documentId, filename) {
try {
// Show loading message
showMessage('PDF wird geladen...', 'success');
// Set PDF title
document.getElementById('pdfViewerTitle').textContent = `📋 ${filename}`;
// Load PDF
const pdfUrl = `/api/dokumente/${documentId}/download`;
const loadingTask = pdfjsLib.getDocument(pdfUrl);
currentPdf = await loadingTask.promise;
totalPages = currentPdf.numPages;
currentPage = 1;
currentScale = 1.0;
// Update UI
document.getElementById('totalPages').textContent = totalPages;
document.getElementById('zoomLevel').textContent = '100%';
// Render first page
await renderPage(currentPage);
// Show modal
const m = document.getElementById('pdfViewerModal');
m.classList.remove('hidden');
m.classList.add('flex');
// Update navigation buttons
updateNavigationButtons();
} catch (error) {
console.error('Fehler beim Laden des PDFs:', error);
showMessage('Fehler beim Laden des PDFs', 'error');
}
}
async function renderPage(pageNum) {
try {
const page = await currentPdf.getPage(pageNum);
const canvas = document.getElementById('pdfCanvas');
const context = canvas.getContext('2d');
// Calculate viewport
const viewport = page.getViewport({ scale: currentScale });
// Set canvas dimensions
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page
const renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext).promise;
// Update page number
document.getElementById('currentPage').textContent = currentPage;
} catch (error) {
console.error('Fehler beim Rendern der Seite:', error);
showMessage('Fehler beim Rendern der Seite', 'error');
}
}
function updateNavigationButtons() {
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
}
async function prevPage() {
if (currentPage > 1) {
currentPage--;
await renderPage(currentPage);
updateNavigationButtons();
}
}
async function nextPage() {
if (currentPage < totalPages) {
currentPage++;
await renderPage(currentPage);
updateNavigationButtons();
}
}
async function zoomIn() {
currentScale += scaleStep;
await renderPage(currentPage);
updateZoomLevel();
}
async function zoomOut() {
if (currentScale > scaleStep) {
currentScale -= scaleStep;
await renderPage(currentPage);
updateZoomLevel();
}
}
function updateZoomLevel() {
const zoomPercent = Math.round(currentScale * 100);
document.getElementById('zoomLevel').textContent = `${zoomPercent}%`;
}
function closePdfViewer() {
const m = document.getElementById('pdfViewerModal');
m.classList.add('hidden');
m.classList.remove('flex');
// Cleanup
currentPdf = null;
currentPage = 1;
totalPages = 0;
currentScale = 1.0;
// Clear canvas
const canvas = document.getElementById('pdfCanvas');
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
}
// Close PDF viewer when clicking outside
document.addEventListener('click', function(event) {
const modal = document.getElementById('pdfViewerModal');
if (event.target === modal) {
closePdfViewer();
}
});
// Keyboard navigation for PDF viewer
document.addEventListener('keydown', function(event) {
const modal = document.getElementById('pdfViewerModal');
if (modal.classList.contains('show')) {
switch(event.key) {
case 'ArrowLeft':
case 'ArrowUp':
event.preventDefault();
prevPage();
break;
case 'ArrowRight':
case 'ArrowDown':
event.preventDefault();
nextPage();
break;
case 'Escape':
event.preventDefault();
closePdfViewer();
break;
case '+':
case '=':
event.preventDefault();
zoomIn();
break;
case '-':
event.preventDefault();
zoomOut();
break;
}
}
});
// Hardware pricing fields toggle and validation
function toggleHardwarePricing() {
const hardwareInput = document.getElementById('hardware');
const hardwarePricingFields = document.getElementById('hardwarePricingFields');
const hardwareEkInput = document.getElementById('hardwareEk');
const kundenverkaufspreisInput = document.getElementById('kundenverkaufspreis');
if (hardwareInput.value.trim() !== '') {
// Reveal the grid wrapper (Tailwind `grid` + `grid-cols-2`).
hardwarePricingFields.classList.remove('hidden');
hardwarePricingFields.classList.add('grid');
hardwareEkInput.required = true;
kundenverkaufspreisInput.required = true;
} else {
hardwarePricingFields.classList.add('hidden');
hardwarePricingFields.classList.remove('grid');
hardwareEkInput.required = false;
kundenverkaufspreisInput.required = false;
hardwareEkInput.value = '';
kundenverkaufspreisInput.value = '';
}
}
// Add hardware pricing validation to form submission
function validateHardwarePricing() {
const hardwareInput = document.getElementById('hardware');
const hardwareEkInput = document.getElementById('hardwareEk');
const kundenverkaufspreisInput = document.getElementById('kundenverkaufspreis');
if (hardwareInput.value.trim() !== '') {
if (!hardwareEkInput.value || parseFloat(hardwareEkInput.value) < 0) {
showMessage('Hardware-EK ist erforderlich wenn Hardware angegeben wird', 'error');
return false;
}
if (!kundenverkaufspreisInput.value || parseFloat(kundenverkaufspreisInput.value) < 0) {
showMessage('Kundenverkaufspreis ist erforderlich wenn Hardware angegeben wird', 'error');
return false;
}
}
return true;
}
// PDF Export Function for Provisionen
function exportProvisionenToPdf() {
try {
// Get current filter values
const anbieterFilter = document.getElementById('filterAnbieterProvisionen')?.value || '';
const tarifFilter = document.getElementById('filterTarifProvisionen')?.value || '';
const schaltungsIdFilter = document.getElementById('filterSchaltungsIdProvisionen')?.value || '';
const rufnummerFilter = document.getElementById('filterRufnummerProvisionen')?.value || '';
const datumVon = document.getElementById('filterDatumVonProvisionen')?.value || '';
const datumBis = document.getElementById('filterDatumBisProvisionen')?.value || '';
// Get filtered data (same logic as applyFiltersProvisionen)
let filteredSchaltungen = [];
if (allProvisionen && allProvisionen.schaltungen) {
filteredSchaltungen = allProvisionen.schaltungen.filter(schaltung => {
// Anbieter filter
if (anbieterFilter && schaltung.anbieter_name !== anbieterFilter) {
return false;
}
// Tarif filter
if (tarifFilter && schaltung.tarif_name !== tarifFilter) {
return false;
}
// Schaltungs-ID filter (supports multiple comma-separated IDs)
if (schaltungsIdFilter) {
const schaltungsIds = schaltungsIdFilter.split(',').map(id => id.trim().toLowerCase()).filter(id => id.length > 0);
if (schaltungsIds.length > 0) {
const schaltungsIdLower = schaltung.schaltungs_id.toLowerCase();
const matches = schaltungsIds.some(searchId => schaltungsIdLower.includes(searchId));
if (!matches) {
return false;
}
}
}
// Rufnummer filter (case insensitive partial match)
if (rufnummerFilter && !schaltung.rufnummer.toLowerCase().includes(rufnummerFilter.toLowerCase())) {
return false;
}
// Date range filter
const schaltungsDatum = new Date(schaltung.schaltungs_datum);
if (datumVon && schaltungsDatum < new Date(datumVon)) {
return false;
}
if (datumBis && schaltungsDatum > new Date(datumBis)) {
return false;
}
return true;
});
}
if (filteredSchaltungen.length === 0) {
showMessage('Keine Daten zum Exportieren gefunden', 'error');
return;
}
// Calculate totals
const totalMitarbeiterProvision = filteredSchaltungen.reduce((sum, row) => {
return sum + parseFloat(row.berechnete_mitarbeiter_provision || 0);
}, 0);
const totalShopProvision = filteredSchaltungen.reduce((sum, row) => {
return sum + parseFloat(row.berechnete_shop_provision || 0);
}, 0);
// Create PDF in landscape orientation
const { jsPDF } = window.jspdf;
const doc = new jsPDF('landscape');
// Add title
doc.setFontSize(18);
doc.setFont(undefined, 'bold');
doc.text('Provisions-Übersicht', 20, 20);
// Add filter info if any filters are applied
let yPos = 35;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
if (anbieterFilter || tarifFilter || datumVon || datumBis) {
doc.text('Aktive Filter:', 20, yPos);
yPos += 6;
if (anbieterFilter) {
doc.text(`• Anbieter: ${anbieterFilter}`, 25, yPos);
yPos += 5;
}
if (tarifFilter) {
doc.text(`• Tarif: ${tarifFilter}`, 25, yPos);
yPos += 5;
}
if (datumVon) {
doc.text(`• Von: ${formatDate(datumVon)}`, 25, yPos);
yPos += 5;
}
if (datumBis) {
doc.text(`• Bis: ${formatDate(datumBis)}`, 25, yPos);
yPos += 5;
}
yPos += 5;
}
// Add generation date
doc.text(`Erstellt am: ${formatDateTime(new Date())}`, 20, yPos);
yPos += 15;
// Table headers
const tableHeaders = [
'Schaltungs-ID',
'Eingabe-Datum',
'Schaltungsdatum',
'Kunde',
'Anbieter',
'Tarif',
'Hardware',
'Hardware-EK',
'Verkaufspreis',
'Shop-Provision',
'Mitarbeiter-Provision'
];
// Set table properties for landscape orientation (11 columns now)
const columnWidths = [20, 20, 20, 35, 25, 30, 20, 20, 20, 25, 25]; // Adjusted for 11 columns
const startX = 10;
const rowHeight = 12; // Increased row height for text wrapping
let currentY = yPos;
// Draw headers
doc.setFontSize(8);
doc.setFont(undefined, 'bold');
let currentX = startX;
tableHeaders.forEach((header, index) => {
doc.rect(currentX, currentY, columnWidths[index], rowHeight);
doc.text(header, currentX + 2, currentY + 8);
currentX += columnWidths[index];
});
currentY += rowHeight;
// Draw data rows
doc.setFont(undefined, 'normal');
// Helper function to wrap text
const wrapText = (text, maxWidth) => {
if (!text) return [''];
const words = text.toString().split(' ');
const lines = [];
let currentLine = '';
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
const testWidth = doc.getTextWidth(testLine);
if (testWidth > maxWidth - 4) { // -4 for padding
if (currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
lines.push(word); // Single word too long, force it
}
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines.length > 0 ? lines : [''];
};
filteredSchaltungen.forEach((schaltung, rowIndex) => {
// Check if we need a new page (landscape has more space)
if (currentY > 180) {
doc.addPage();
currentY = 20;
// Redraw headers on new page
doc.setFont(undefined, 'bold');
currentX = startX;
tableHeaders.forEach((header, index) => {
doc.rect(currentX, currentY, columnWidths[index], rowHeight);
doc.text(header, currentX + 2, currentY + 8);
currentX += columnWidths[index];
});
currentY += rowHeight;
doc.setFont(undefined, 'normal');
}
// Prepare row data
const rawData = [
schaltung.schaltungs_id || '',
schaltung.eingabe_datum ? formatDate(schaltung.eingabe_datum) : '',
schaltung.schaltungs_datum ? formatDate(schaltung.schaltungs_datum) : '',
`${schaltung.vorname || ''} ${schaltung.nachname || ''}`.trim() + (schaltung.firma ? ` (${schaltung.firma})` : ''),
schaltung.anbieter_name || '',
schaltung.tarif_name || '',
schaltung.hardware || '-',
schaltung.hardware_ek ? `${parseFloat(schaltung.hardware_ek).toFixed(2)} €` : '-',
schaltung.kundenverkaufspreis_netto ? `${parseFloat(schaltung.kundenverkaufspreis_netto).toFixed(2)} €` : '-',
`${schaltung.berechnete_shop_provision || '0.00'} €`,
`${schaltung.berechnete_mitarbeiter_provision || '0.00'} €`
];
// Wrap text for each column
const wrappedData = rawData.map((data, index) => wrapText(data, columnWidths[index]));
// Calculate actual row height needed
const maxLines = Math.max(...wrappedData.map(lines => lines.length));
const actualRowHeight = Math.max(rowHeight, maxLines * 4 + 4);
currentX = startX;
// Draw row with proper text wrapping
wrappedData.forEach((lines, colIndex) => {
// Draw cell border
doc.rect(currentX, currentY, columnWidths[colIndex], actualRowHeight);
// Draw text lines
lines.forEach((line, lineIndex) => {
const textY = currentY + 6 + (lineIndex * 4);
doc.text(line, currentX + 2, textY);
});
currentX += columnWidths[colIndex];
});
currentY += actualRowHeight;
});
// Add totals
currentY += 10;
doc.setFont(undefined, 'bold');
doc.setFontSize(10);
doc.text(`Gesamte Shop-Provision: ${totalShopProvision.toFixed(2)} €`, startX, currentY);
currentY += 8;
doc.text(`Gesamte Mitarbeiter-Provision: ${totalMitarbeiterProvision.toFixed(2)} €`, startX, currentY);
currentY += 8;
doc.text(`Gesamt: ${(totalShopProvision + totalMitarbeiterProvision).toFixed(2)} €`, startX, currentY);
// Generate filename with current date and filters
let filename = 'Provisionen_';
if (datumVon || datumBis) {
if (datumVon) filename += `von_${datumVon}_`;
if (datumBis) filename += `bis_${datumBis}_`;
}
filename += new Date().toISOString().split('T')[0] + '.pdf';
// Save the PDF
doc.save(filename);
showMessage('PDF erfolgreich erstellt und heruntergeladen!', 'success');
} catch (error) {
console.error('Fehler beim PDF-Export:', error);
showMessage('Fehler beim Erstellen des PDFs', 'error');
}
}
// Add form validation for hardware pricing
document.addEventListener('DOMContentLoaded', function() {
const schaltungForm = document.getElementById('schaltungForm');
if (schaltungForm) {
schaltungForm.addEventListener('submit', function(event) {
if (!validateHardwarePricing()) {
event.preventDefault();
return false;
}
});
}
});
// ============================================================================
// Benutzer-Verwaltung (admin only). Backend: /api/users/*
// ============================================================================
async function loadUsers() {
const tbody = document.getElementById('usersTableBody');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="5" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Lade…</td></tr>';
try {
const users = await apiRequest('/api/users');
tbody.innerHTML = '';
if (!users.length) {
tbody.innerHTML = '<tr><td colspan="5" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Keine Benutzer</td></tr>';
return;
}
users.forEach(u => {
const isSelf = currentUser && currentUser.username === u.username;
const tr = document.createElement('tr');
tr.className = 'border-b border-outline-variant/30 even:bg-surface-container-low/50 hover:bg-surface-variant/30 transition-colors';
tr.innerHTML = `
<td class="px-md py-xs">
<span class="font-semibold text-on-background">${escapeHtml(u.username)}</span>
${isSelf ? '<span class="ml-xs text-label-caps text-primary">Sie</span>' : ''}
</td>
<td class="px-md py-xs text-on-surface">${escapeHtml(u.displayName || '–')}</td>
<td class="px-md py-xs text-center">
${u.isAdmin
? '<span class="material-symbols-outlined text-primary text-[20px]" title="Administrator">verified_user</span>'
: '<span class="text-on-surface-variant">—</span>'}
</td>
<td class="px-md py-xs font-label-numeric text-label-numeric text-on-surface-variant">${escapeHtml(formatDateTime(u.createdAt))}</td>
<td class="px-md py-xs">
<div class="flex items-center justify-center gap-sm">
<button data-user-action="edit" data-uuid="${escapeHtml(u.uuid)}" title="Bearbeiten"
class="text-outline hover:text-primary transition-colors">
<span class="material-symbols-outlined text-[18px]">edit</span>
</button>
<button data-user-action="delete" data-uuid="${escapeHtml(u.uuid)}" data-username="${escapeHtml(u.username)}" title="${isSelf ? 'Eigener Account kann nicht gelöscht werden' : 'Löschen'}"
class="text-outline hover:text-error transition-colors ${isSelf ? 'opacity-40 cursor-not-allowed' : ''}"
${isSelf ? 'disabled' : ''}>
<span class="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
</td>
`;
tr.querySelectorAll('button[data-user-action]').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.disabled) return;
const action = btn.dataset.userAction;
const uuid = btn.dataset.uuid;
if (action === 'edit') openUserModal(u);
else if (action === 'delete') deleteUser(uuid, btn.dataset.username);
});
});
tbody.appendChild(tr);
});
} catch (err) {
tbody.innerHTML = '<tr><td colspan="5" class="px-md py-lg text-center text-body-sm text-error">Fehler beim Laden</td></tr>';
}
}
function openUserModal(user) {
const isEdit = !!user;
document.getElementById('userModalTitle').textContent = isEdit ? 'Benutzer bearbeiten' : 'Neuer Benutzer';
document.getElementById('userModalUuid').value = isEdit ? user.uuid : '';
document.getElementById('userModalUsername').value = isEdit ? user.username : '';
document.getElementById('userModalDisplay').value = isEdit ? (user.displayName || '') : '';
document.getElementById('userModalPassword').value = '';
document.getElementById('userModalIsAdmin').checked = isEdit ? !!user.isAdmin : false;
document.getElementById('userModalPasswordHint').style.display = isEdit ? 'none' : '';
document.getElementById('userModalPasswordEditHint').style.display = isEdit ? '' : 'none';
document.getElementById('userModal').style.display = 'block';
}
function closeUserModal() {
document.getElementById('userModal').style.display = 'none';
}
async function saveUserFromModal() {
const uuid = document.getElementById('userModalUuid').value;
const username = document.getElementById('userModalUsername').value.trim();
const displayName = document.getElementById('userModalDisplay').value.trim();
const password = document.getElementById('userModalPassword').value;
const isAdmin = document.getElementById('userModalIsAdmin').checked;
if (!username) {
showMessage('Username ist erforderlich', 'error');
return;
}
try {
if (uuid) {
// Update existing user
const body = { username, displayName, isAdmin };
if (password) body.newPassword = password;
await apiRequest(`/api/users/${encodeURIComponent(uuid)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
showMessage(`Benutzer "${username}" aktualisiert`);
} else {
// Create new user
if (!password || password.length < 8) {
showMessage('Passwort muss mindestens 8 Zeichen lang sein', 'error');
return;
}
await apiRequest('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, displayName, isAdmin }),
});
showMessage(`Benutzer "${username}" angelegt`);
}
closeUserModal();
loadUsers();
} catch (err) {
// apiRequest already shows the error message
}
}
async function deleteUser(uuid, username) {
if (!confirm(`Benutzer "${username}" wirklich löschen?`)) return;
try {
await apiRequest(`/api/users/${encodeURIComponent(uuid)}`, { method: 'DELETE' });
showMessage(`Benutzer "${username}" gelöscht`);
loadUsers();
} catch (err) {
// apiRequest already shows the error message
}
}
// ============================================================================
// VO-Verwaltung (Vertriebspartner-Nummern). Backend: /api/vos
// ============================================================================
async function loadAnbieterForVoForm() {
try {
const anbieter = await apiRequest('/api/anbieter');
const select = document.getElementById('voAnbieter');
if (!select) return;
select.innerHTML = '<option value="">Anbieter wählen...</option>';
anbieter.forEach(a => {
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
});
} catch (e) { /* error already shown */ }
}
async function loadVosAdmin() {
const tbody = document.getElementById('vosList');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#666;">Lade...</td></tr>';
try {
// Load anbieter to know each one's default_vo_id.
const [vos, anbieter] = await Promise.all([
apiRequest('/api/vos'),
apiRequest('/api/anbieter'),
]);
const defaultByAnbieter = {};
anbieter.forEach(a => { defaultByAnbieter[a.id] = a.default_vo_id; });
tbody.innerHTML = '';
if (!vos.length) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#666;">Keine VOs vorhanden</td></tr>';
return;
}
vos.forEach(v => {
const isDefault = defaultByAnbieter[v.anbieter_id] === v.id;
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${escapeHtml(v.anbieter_name)}</td>
<td>${escapeHtml(v.nummer)}</td>
<td>${escapeHtml(v.bezeichnung || '')}</td>
<td>${v.aktiv ? '<span style="color:#27ae60;">Ja</span>' : '<span style="color:#c0392b;">Nein</span>'}</td>
<td>${isDefault ? '<span title="Standard-VO" style="color:#f39c12; font-size:18px;">★</span>' : ''}</td>
<td>
${isDefault
? `<button data-vo-action="unset-default" data-anbieter-id="${v.anbieter_id}" style="margin-right:5px;">Standard entfernen</button>`
: `<button data-vo-action="set-default" data-anbieter-id="${v.anbieter_id}" data-id="${v.id}" style="margin-right:5px;">Als Standard</button>`}
<button data-vo-action="edit" data-id="${v.id}"
data-anbieter-name="${escapeHtml(v.anbieter_name)}"
data-nummer="${escapeHtml(v.nummer)}"
data-bezeichnung="${escapeHtml(v.bezeichnung || '')}"
data-aktiv="${v.aktiv}" class="btn-success" style="margin-right:5px;">Bearbeiten</button>
<button data-vo-action="delete" data-id="${v.id}" data-label="${escapeHtml(v.nummer + (v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''))}" class="btn-danger">Löschen</button>
</td>
`;
tr.querySelectorAll('button[data-vo-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.voAction;
if (action === 'edit') {
editVo(parseInt(btn.dataset.id, 10), btn.dataset.nummer, btn.dataset.bezeichnung, btn.dataset.aktiv === '1', btn.dataset.anbieterName);
} else if (action === 'delete') {
deleteVo(parseInt(btn.dataset.id, 10), btn.dataset.label);
} else if (action === 'set-default') {
setAnbieterDefaultVo(parseInt(btn.dataset.anbieterId, 10), parseInt(btn.dataset.id, 10));
} else if (action === 'unset-default') {
setAnbieterDefaultVo(parseInt(btn.dataset.anbieterId, 10), null);
}
});
});
tbody.appendChild(tr);
});
} catch (e) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#c0392b;">Fehler beim Laden</td></tr>';
}
}
async function setAnbieterDefaultVo(anbieterId, voId) {
try {
await apiRequest(`/api/anbieter/${anbieterId}/default-vo`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vo_id: voId }),
});
showMessage(voId ? 'Standard-VO gesetzt' : 'Standard-VO entfernt');
loadVosAdmin();
} catch (e) { /* error already shown */ }
}
async function addVo() {
const anbieter_id = document.getElementById('voAnbieter').value;
const nummer = document.getElementById('voNummer').value.trim();
const bezeichnung = document.getElementById('voBezeichnung').value.trim();
if (!anbieter_id || !nummer) {
showMessage('Anbieter und VO-Nummer sind erforderlich', 'error');
return;
}
try {
await apiRequest('/api/vos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ anbieter_id, nummer, bezeichnung }),
});
document.getElementById('voAnbieter').value = '';
document.getElementById('voNummer').value = '';
document.getElementById('voBezeichnung').value = '';
showMessage('VO erfolgreich hinzugefügt');
loadVosAdmin();
} catch (e) { /* already shown */ }
}
function editVo(id, nummer, bezeichnung, aktiv, anbieterName) {
document.getElementById('voEditId').value = id;
document.getElementById('voEditAnbieter').value = anbieterName || '';
document.getElementById('voEditNummer').value = nummer || '';
document.getElementById('voEditBezeichnung').value = bezeichnung || '';
document.getElementById('voEditAktiv').checked = !!aktiv;
document.getElementById('voEditModal').style.display = 'block';
}
function closeVoEditModal() {
document.getElementById('voEditModal').style.display = 'none';
}
async function saveVoFromModal() {
const id = document.getElementById('voEditId').value;
const nummer = document.getElementById('voEditNummer').value.trim();
const bezeichnung = document.getElementById('voEditBezeichnung').value.trim();
const aktiv = document.getElementById('voEditAktiv').checked;
if (!nummer) {
showMessage('VO-Nummer darf nicht leer sein', 'error');
return;
}
try {
await apiRequest(`/api/vos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nummer, bezeichnung: bezeichnung || null, aktiv }),
});
closeVoEditModal();
showMessage('VO aktualisiert');
loadVosAdmin();
} catch (e) { /* already shown */ }
}
async function deleteVo(id, label) {
if (!confirm(`VO "${label}" wirklich löschen?`)) return;
try {
await apiRequest(`/api/vos/${id}`, { method: 'DELETE' });
showMessage('VO gelöscht');
loadVosAdmin();
} catch (e) { /* already shown */ }
}