The Anzeigen / Download / Löschen icons inside the documents modal and the close X of every modal sit outside <table>, so the previous "table button" reset didn't reach them — the global blue button styling shone through. - Introduce a generic `.icon-btn` class that mirrors the table reset (transparent background, no padding, inherit colour). The reset rule is now `table button, .icon-btn`. - Tag the documents modal close button and the view/download/delete buttons inside each document card with `icon-btn`. Tailwind classes (`text-outline hover:text-primary` etc.) still win for colour. - Rebuild the PDF viewer modal in Stitch style: clean white card, light header with picture_as_pdf glyph + title, body on a soft grey surface, footer with chevron navigation buttons and zoom icons. Toggle hidden/flex instead of the legacy `.show` class. Removed the matching .pdf-viewer-* / .pdf-nav-btn / .pdf-zoom-btn CSS that no longer applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4218 lines
167 KiB
JavaScript
4218 lines
167 KiB
JavaScript
// 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// 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') {
|
||
// Check if data is already loaded, if not load it
|
||
if (!document.getElementById('geloeschteList') || !document.getElementById('geloeschteList').innerHTML.trim()) {
|
||
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');
|
||
|
||
tbody.innerHTML = '';
|
||
|
||
geloeschte.forEach(s => {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${escapeHtml(s.schaltungs_id)}</td>
|
||
<td>${escapeHtml(formatDate(s.eingabe_datum))}</td>
|
||
<td>${escapeHtml(formatDate(s.schaltungs_datum))}</td>
|
||
<td>${escapeHtml(s.vorname)} ${escapeHtml(s.nachname)}</td>
|
||
<td>${escapeHtml(s.firma || '-')}</td>
|
||
<td>${escapeHtml(s.rufnummer)}</td>
|
||
<td>${escapeHtml(s.anbieter_name)}</td>
|
||
<td>${escapeHtml(s.tarif_name)}</td>
|
||
<td>${escapeHtml(s.hardware || '')}</td>
|
||
<td><strong style="color: #e74c3c;">${escapeHtml(s.loeschgrund)}</strong></td>
|
||
<td>${escapeHtml(formatDateTime(s.geloescht_am))}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
} 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);
|
||
}
|
||
|
||
// Update Provisionen table with filtered data
|
||
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.length === 0) {
|
||
// Show "no results" message
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td colspan="11" style="text-align: center; padding: 40px; color: #7f8c8d; font-style: italic; font-size: 16px;">
|
||
💰 Keine Provisionen entsprechen den gewählten Filterkriterien
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
|
||
// Reset totals to 0
|
||
if (totalShopSpan) totalShopSpan.textContent = '0,00 €';
|
||
if (totalMitarbeiterSpan) totalMitarbeiterSpan.textContent = '0,00 €';
|
||
return;
|
||
}
|
||
|
||
schaltungen.forEach(s => {
|
||
const row = document.createElement('tr');
|
||
|
||
row.innerHTML = `
|
||
<td>${escapeHtml(s.schaltungs_id)}</td>
|
||
<td>${escapeHtml(formatDate(s.schaltungs_datum))}</td>
|
||
<td>${escapeHtml(s.vorname)} ${escapeHtml(s.nachname)}</td>
|
||
<td>${escapeHtml(s.anbieter_name)}</td>
|
||
<td>${escapeHtml(s.tarif_name)}</td>
|
||
<td><strong>${escapeHtml(s.berechnete_shop_provision || '0.00')} €</strong></td>
|
||
<td><strong>${escapeHtml(s.berechnete_mitarbeiter_provision || '0.00')} €</strong></td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
// Update totals
|
||
if (totalShopSpan) totalShopSpan.textContent = `${totalShopProvision.toFixed(2)} €`;
|
||
if (totalMitarbeiterSpan) totalMitarbeiterSpan.textContent = `${totalMitarbeiterProvision.toFixed(2)} €`;
|
||
}
|
||
|
||
// 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() !== '') {
|
||
// Show pricing fields and make them required
|
||
hardwarePricingFields.style.display = 'block';
|
||
hardwareEkInput.required = true;
|
||
kundenverkaufspreisInput.required = true;
|
||
} else {
|
||
// Hide pricing fields and remove required attribute
|
||
hardwarePricingFields.style.display = 'none';
|
||
hardwareEkInput.required = false;
|
||
kundenverkaufspreisInput.required = false;
|
||
// Clear values
|
||
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" style="text-align:center; color:#666;">Lade...</td></tr>';
|
||
try {
|
||
const users = await apiRequest('/api/users');
|
||
tbody.innerHTML = '';
|
||
if (!users.length) {
|
||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#666;">Keine Benutzer</td></tr>';
|
||
return;
|
||
}
|
||
users.forEach(u => {
|
||
const tr = document.createElement('tr');
|
||
const isSelf = currentUser && currentUser.username === u.username;
|
||
tr.innerHTML = `
|
||
<td>${escapeHtml(u.username)}${isSelf ? ' <small style="color:#3498db;">(Sie)</small>' : ''}</td>
|
||
<td>${escapeHtml(u.displayName || '')}</td>
|
||
<td>${u.isAdmin ? '<span style="color:#27ae60;">✓</span>' : ''}</td>
|
||
<td>${escapeHtml(formatDateTime(u.createdAt))}</td>
|
||
<td>
|
||
<button data-user-action="edit" data-uuid="${escapeHtml(u.uuid)}" class="btn-success" style="margin-right:5px;">Bearbeiten</button>
|
||
<button data-user-action="delete" data-uuid="${escapeHtml(u.uuid)}" data-username="${escapeHtml(u.username)}" class="btn-danger" ${isSelf ? 'disabled title="Eigener Account kann nicht gelöscht werden"' : ''}>Löschen</button>
|
||
</td>
|
||
`;
|
||
tr.querySelectorAll('button[data-user-action]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
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" style="text-align:center; color:#c0392b;">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 */ }
|
||
}
|