Backend - New routes/users.js (admin-only via requireAdmin) with list, create, update (incl. password reset) and delete endpoints. - All user-facing API responses identify users by uuid, not the numeric primary key — usernames can now be renamed without breaking external references. - Protection rules: cannot delete or demote the last admin; cannot delete or demote your own account. Schema / data - users table gained a uuid TEXT column (unique). Existing rows are backfilled with crypto.randomUUID() on startup. Setup endpoint and the create-user CLI both generate a UUID for new users. Frontend - New "Benutzer" tab in nav-tabs, marked admin-only and shown only after /api/auth/me reports isAdmin. - Modal with create / edit forms (password optional on edit = leave unchanged). XSS-safe rendering of the user list using the existing escapeHtml helper and data-* attributes for action buttons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3874 lines
150 KiB
JavaScript
3874 lines
150 KiB
JavaScript
// Global variables
|
|
let currentSchaltungId = null;
|
|
let uploadedFiles = [];
|
|
let currentUser = null;
|
|
|
|
// Escape user-controlled values before embedding into innerHTML. Replaces all five
|
|
// XSS-relevant characters; safe inside both text content and double-quoted attributes.
|
|
function escapeHtml(value) {
|
|
if (value === null || value === undefined) return '';
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// Initialize the application with background loading
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
// Verify session before loading anything else; redirect to /login on 401.
|
|
try {
|
|
const meRes = await fetch('/api/auth/me', { credentials: 'same-origin' });
|
|
if (meRes.status === 401) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
currentUser = await meRes.json();
|
|
renderUserBadge();
|
|
} catch (e) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
|
|
loadAnbieter();
|
|
loadSchaltungen();
|
|
setupFileUpload();
|
|
|
|
// Set today's date as default
|
|
const today = new Date().toISOString().split('T')[0];
|
|
document.getElementById('eingabeDatum').value = today;
|
|
|
|
// Setup form submission
|
|
document.getElementById('schaltungForm').addEventListener('submit', handleSchaltungSubmit);
|
|
|
|
// Load all data in background for faster tab switching
|
|
preloadAllData();
|
|
});
|
|
|
|
function renderUserBadge() {
|
|
if (!currentUser) return;
|
|
// Show admin-only navigation elements (e.g. the Benutzer tab) once we know the role.
|
|
document.querySelectorAll('.admin-only').forEach(el => {
|
|
el.style.display = currentUser.isAdmin ? '' : 'none';
|
|
});
|
|
let badge = document.getElementById('userBadge');
|
|
if (!badge) {
|
|
badge = document.createElement('div');
|
|
badge.id = 'userBadge';
|
|
badge.style.cssText = 'position:fixed;top:12px;right:16px;z-index:9000;background:rgba(255,255,255,0.95);padding:8px 14px;border-radius:6px;font-size:13px;box-shadow:0 2px 6px rgba(0,0,0,0.15);font-family:Segoe UI,Tahoma,sans-serif;';
|
|
document.body.appendChild(badge);
|
|
}
|
|
const name = currentUser.displayName || currentUser.username;
|
|
badge.innerHTML = '';
|
|
const span = document.createElement('span');
|
|
span.textContent = name + ' · ';
|
|
const link = document.createElement('a');
|
|
link.href = '#';
|
|
link.textContent = 'Abmelden';
|
|
link.style.color = '#c0392b';
|
|
link.style.textDecoration = 'none';
|
|
link.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); } catch (_) {}
|
|
window.location.href = '/login';
|
|
});
|
|
badge.appendChild(span);
|
|
badge.appendChild(link);
|
|
}
|
|
|
|
// 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');
|
|
|
|
const anbieterSelect = document.getElementById('anbieter');
|
|
const tarifAnbieterSelect = document.getElementById('tarifAnbieter');
|
|
|
|
// Clear existing options
|
|
anbieterSelect.innerHTML = '<option value="">Bitte wählen...</option>';
|
|
tarifAnbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
|
|
|
|
anbieter.forEach(a => {
|
|
anbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
|
tarifAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
|
});
|
|
|
|
} 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());
|
|
|
|
// Collect selected tarifoptionen
|
|
const selectedTarifoptionen = Array.from(document.querySelectorAll('input[name="tarifoptionen"]:checked'))
|
|
.map(checkbox => parseInt(checkbox.value));
|
|
|
|
// Validate hardware length if provided
|
|
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;
|
|
|
|
// Add selected tarifoptionen to the schaltung
|
|
if (selectedTarifoptionen.length > 0) {
|
|
await addTarifoptionenToSchaltung(currentSchaltungId, selectedTarifoptionen);
|
|
}
|
|
|
|
// Upload files if any
|
|
if (uploadedFiles.length > 0) {
|
|
await uploadDocuments(currentSchaltungId);
|
|
}
|
|
|
|
showMessage('Schaltung erfolgreich gespeichert!');
|
|
|
|
// Reset form
|
|
e.target.reset();
|
|
uploadedFiles = [];
|
|
document.getElementById('uploadedFiles').innerHTML = '';
|
|
hideTarifoptionen(); // Hide tarifoptionen section
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
document.getElementById('eingabeDatum').value = today;
|
|
|
|
// Reload overview and provisions if needed
|
|
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);
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('de-DE');
|
|
}
|
|
|
|
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 '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 = new Date(a.created_at).toLocaleDateString('de-DE');
|
|
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="6" 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 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(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="6" 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 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;
|
|
}
|
|
|
|
// Validation for provision euro
|
|
if (provisionEuro && parseFloat(provisionEuro) < 0) {
|
|
showMessage('Provision kann nicht negativ sein', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const requestData = {
|
|
name,
|
|
anbieter_id: anbieterId,
|
|
provision_euro: provisionEuro ? parseFloat(provisionEuro) : 0,
|
|
beschreibung: beschreibung || null
|
|
};
|
|
|
|
await apiRequest('/api/tarife', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
// Reset form
|
|
document.getElementById('newTarif').value = '';
|
|
document.getElementById('tarifAnbieter').value = '';
|
|
document.getElementById('tarifProvisionBetrag').value = '';
|
|
document.getElementById('tarifBeschreibung').value = '';
|
|
|
|
showMessage('Tarif erfolgreich hinzugefügt');
|
|
|
|
// Reload data
|
|
await loadTarifeAdmin();
|
|
await loadTarifeForProvision();
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Hinzufügen des Tarifs:', error);
|
|
showMessage('Fehler beim Hinzufügen des Tarifs', 'error');
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// Get all tariffs from the provider
|
|
const anbieter = await apiRequest('/api/anbieter');
|
|
let tariff = null;
|
|
|
|
for (const a of anbieter) {
|
|
const tarife = await apiRequest(`/api/tarife/${a.id}`);
|
|
tariff = tarife.find(t => t.id == id);
|
|
if (tariff) break;
|
|
}
|
|
|
|
if (!tariff) {
|
|
showMessage('Tarif nicht gefunden', 'error');
|
|
return;
|
|
}
|
|
|
|
// Fill modal with tariff data
|
|
document.getElementById('editTariffId').value = tariff.id;
|
|
document.getElementById('editTariffName').value = tariff.name;
|
|
document.getElementById('editTariffProvisionBetrag').value = tariff.provision_euro || '';
|
|
|
|
|
|
// Show modal
|
|
document.getElementById('editTariffModal').style.display = 'block';
|
|
|
|
// Setup form submission
|
|
document.getElementById('editTariffForm').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
|
|
const formData = {
|
|
name: document.getElementById('editTariffName').value.trim(),
|
|
provision_euro: document.getElementById('editTariffProvisionBetrag').value ? parseFloat(document.getElementById('editTariffProvisionBetrag').value) : 0
|
|
};
|
|
|
|
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) {
|
|
const date = new Date(dateTimeString);
|
|
return date.toLocaleDateString('de-DE') + ' ' + date.toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'});
|
|
}
|
|
|
|
// 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(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: ${new Date(datumVon).toLocaleDateString('de-DE')}`, 25, yPos);
|
|
yPos += 5;
|
|
}
|
|
if (datumBis) {
|
|
doc.text(`• Bis: ${new Date(datumBis).toLocaleDateString('de-DE')}`, 25, yPos);
|
|
yPos += 5;
|
|
}
|
|
yPos += 5;
|
|
}
|
|
|
|
// Add generation date
|
|
doc.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE')} um ${new Date().toLocaleTimeString('de-DE')}`, 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 ? new Date(schaltung.eingabe_datum).toLocaleDateString('de-DE') : '',
|
|
schaltung.schaltungs_datum ? new Date(schaltung.schaltungs_datum).toLocaleDateString('de-DE') : '',
|
|
`${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(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
|
|
}
|
|
}
|