Files
prov/public/script.js
T
kerimatilganandClaude Opus 4.7 a535dcee38 UI redesign step 1/N: Tailwind foundation + Stitch theme + Login + Wizard
Foundation
- Pull in Tailwind via CDN with the full Stitch design tokens
  (palette, typography, spacing, radius) inline in <head>. Preflight
  disabled so the rest of the legacy inline CSS keeps applying until
  each screen is migrated.
- Add Hanken Grotesk + JetBrains Mono + Material Symbols Outlined
  via Google Fonts.
- Replace the page header with the Stitch dark-slate top bar: brand
  on the left, segmented tab nav in the middle, user pill + logout
  on the right. Floating userBadge removed; renderUserBadge() now
  writes the display name into #headerUserName and wires the
  existing logout button.

Login
- Single card mirroring the Stitch login mockup with icon-prefixed
  inputs (person/lock), focus ring, error banner and a primary CTA
  that includes the login glyph. Setup mode swaps the heading,
  shows the orange info bar with admin_panel_settings glyph and
  changes the button to "Administrator anlegen".

Wizard (Schaltung erfassen)
- Step 1: anbieter button grid in Stitch style — bordered cards
  with a material icon, hover lift and primary-coloured outline.
- Step 2: white card with a "← Anbieter ändern" pill in the
  header, two-column layout (Aktivierungsdaten / Vertragsdaten),
  label-caps form labels, JetBrains-Mono numeric inputs for
  dates/ID/phone and a dashed file-drop zone. Save button gains
  the save glyph.

Other screens (Übersicht, Provisionen, Gelöschte, Administration,
Benutzer) still render in the legacy style — those migrations
follow in the next steps.

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

4233 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Initialize the application with background loading
document.addEventListener('DOMContentLoaded', async function() {
// Verify session before loading anything else; redirect to /login on 401.
try {
const meRes = await fetch('/api/auth/me', { credentials: 'same-origin' });
if (meRes.status === 401) {
window.location.href = '/login';
return;
}
currentUser = await meRes.json();
renderUserBadge();
} catch (e) {
window.location.href = '/login';
return;
}
loadAnbieter();
loadSchaltungen();
setupFileUpload();
renderWizardAnbieterButtons();
// Set today's date as default
const today = new Date().toISOString().split('T')[0];
document.getElementById('eingabeDatum').value = today;
document.getElementById('schaltungsDatum').value = today;
// Erfasser autofill
const erfasser = document.getElementById('wizardErfasser');
if (erfasser) erfasser.value = currentUser.displayName || currentUser.username;
document.getElementById('schaltungForm').addEventListener('submit', handleSchaltungSubmit);
preloadAllData();
});
// --- Schaltung Wizard ---
async function renderWizardAnbieterButtons() {
const container = document.getElementById('wizardAnbieterButtons');
if (!container) return;
container.innerHTML = '<div class="text-body-sm text-on-surface-variant col-span-full">Lade...</div>';
try {
const anbieter = await apiRequest('/api/anbieter');
container.innerHTML = '';
if (!anbieter.length) {
container.innerHTML = '<div class="text-body-sm text-on-surface-variant col-span-full">Keine Anbieter vorhanden. Bitte zuerst unter Administration einen Anbieter anlegen.</div>';
return;
}
anbieter.forEach(a => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'group flex flex-col items-center justify-center p-lg border border-outline-variant rounded-lg 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');
const tbody = document.querySelector('#schaltungenTable tbody');
tbody.innerHTML = '';
schaltungen.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><button data-action="documents" data-id="${s.id}" class="btn-success">Dokumente</button></td>
<td>
<button data-action="edit" data-id="${s.id}" class="btn-success" style="margin-right: 5px;">Bearbeiten</button>
<button data-action="delete" data-id="${s.id}" data-schaltungs-id="${escapeHtml(s.schaltungs_id)}" data-name="${escapeHtml(s.vorname + ' ' + s.nachname)}" class="btn-danger">Löschen</button>
</td>
`;
// Wire delegated handlers so we don't need inline onclick (avoids JS-injection via names).
row.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(row);
});
} catch (error) {
console.error('Fehler beim Laden der Schaltungen:', error);
}
}
// 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) {
modalBody.innerHTML = `
<div class="no-documents">
<div class="no-documents-icon">📄</div>
<div>Keine Dokumente vorhanden</div>
<div style="font-size: 14px; margin-top: 10px;">Für diese Schaltung wurden noch keine Dokumente hochgeladen.</div>
<div style="margin-top: 15px;">
<button class="btn btn-primary" onclick="document.getElementById('additionalDocumentUpload').click()"
style="background: #007bff; border-color: #007bff;">
📎 Erste Dokumente hinzufügen
</button>
</div>
</div>
`;
} else {
documents.forEach(doc => {
const docItem = document.createElement('div');
docItem.className = 'document-item';
// Get file extension for icon
const fileExt = doc.original_filename.split('.').pop().toLowerCase();
let icon = '📄';
if (['pdf'].includes(fileExt)) icon = '📋';
else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) icon = '🖼️';
else if (['doc', 'docx'].includes(fileExt)) icon = '📝';
// Format file size
const fileSize = doc.filesize ? `${(doc.filesize / 1024).toFixed(1)} KB` : 'Unbekannt';
// Format upload date
const uploadDate = doc.uploaded_at ? formatDate(doc.uploaded_at) : 'Unbekannt';
const showView = fileExt === 'pdf';
const safeName = escapeHtml(doc.original_filename);
docItem.innerHTML = `
<div class="document-info">
<div class="document-icon">${icon}</div>
<div class="document-details">
<div class="document-name">${safeName}</div>
<div class="document-meta">Größe: ${escapeHtml(fileSize)} • Hochgeladen: ${escapeHtml(uploadDate)}</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
${showView ? `<button class="view-btn" data-doc-action="view" data-id="${doc.id}" data-name="${safeName}">👁️ Anzeigen</button>` : ''}
<button class="download-btn" data-doc-action="download" data-id="${doc.id}">
📥 Download
</button>
<button class="btn-danger" data-doc-action="delete" data-id="${doc.id}" data-name="${safeName}" data-schaltung-id="${schaltungId}"
style="background: #dc3545; border-color: #dc3545; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px;"
title="Dokument löschen">
🗑️
</button>
</div>
`;
docItem.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(docItem);
});
}
// Setup file upload handler for additional documents
const fileInput = document.getElementById('additionalDocumentUpload');
fileInput.onchange = function(event) {
uploadAdditionalDocuments(schaltungId, event.target.files);
};
modal.classList.add('show');
} 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.remove('show');
}
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;
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 Schaltungen entsprechen den gewählten Filterkriterien
</td>
`;
tbody.appendChild(row);
return;
}
schaltungen.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>
<button data-filt-action="documents" data-id="${s.id}" class="btn-success">Dokumente</button>
</td>
<td>
<button data-filt-action="edit" data-id="${s.id}" class="btn-secondary">Bearbeiten</button>
<button data-filt-action="delete" data-id="${s.id}" data-schaltungs-id="${escapeHtml(s.schaltungs_id)}" data-name="${escapeHtml(s.vorname + ' ' + s.nachname)}" class="btn-danger">Löschen</button>
</td>
`;
row.querySelectorAll('button[data-filt-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.filtAction;
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(row);
});
}
// 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
document.getElementById('pdfViewerModal').classList.add('show');
// 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() {
document.getElementById('pdfViewerModal').classList.remove('show');
// 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 */ }
}