Major refactor: auth, bug fixes, security hardening, modular routes
Bug fixes - Sync database.js schema with actual production DB (tarifoptionen shape was wrong; new installs would have broken). - Fix SQL syntax error `SELECT to.*` (reserved keyword) in single- schaltung provision endpoint, plus the dead fallback hierarchy that selected columns that were never in the query. - Fix PUT /api/tarifoptionen/:id writing to non-existent column provision_betrag (should be provision_euro). - Fix race condition in PUT /api/schaltungen/:id where forEach with async db.run inside a transaction could send duplicate responses. - Delete dokumente rows explicitly before deleting schaltung (existing DBs lack ON DELETE CASCADE on dokumente FK). - Replace hardcoded basisBetrag=100 for percent rules; basis is now configurable per rule via the new basis_euro column. Security - Multi-user authentication: users table, bcryptjs, express-session with SQLite store, login/logout/me/setup/change-password endpoints. - Auth gate on all /api/* except /api/auth/*. - CLI to create users: npm run create-user -- <user> <pass> [--admin]. - helmet for security headers. - CORS restricted to ALLOWED_ORIGINS env (default same-origin). - Rate limits: 300/min global, 20/15min on login. - Path-traversal validation on document download/delete. - XSS hardening in frontend: escapeHtml helper applied to all DB-derived innerHTML interpolations; inline onclick handlers that embedded user-controlled strings replaced with data-* attrs + addEventListener. Architecture - Provision calculation centralised in provisionService.js as the single source of truth (was duplicated and inconsistent). - server.js cut from 2138 to ~140 lines; routes split into per- resource modules under routes/. - Promise wrapper db.js for sqlite3 callbacks; async/await in routes. - middleware/ for auth and async error handling. Tests - 14 unit tests for provisionService covering fallback hierarchy, percent calculation, tarifoptionen, hardware logic, employee commission. All green. Config - dotenv loads .env.local at startup (gitignored). - .gitignore extended for sessions.db and .env*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
55ec916f35
commit
4303b00b9b
23 files changed
+2427
-2575
No files matched your search
+307
-180
@@ -1,24 +1,78 @@
|
||||
// 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', function() {
|
||||
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;
|
||||
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...');
|
||||
@@ -93,23 +147,29 @@ function showTab(tabName) {
|
||||
// API Helper functions
|
||||
async function apiRequest(url, options = {}) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
// Check if response has content
|
||||
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');
|
||||
@@ -215,8 +275,8 @@ async function loadAnbieter() {
|
||||
tarifAnbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
|
||||
anbieter.forEach(a => {
|
||||
anbieterSelect.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
tarifAnbieterSelect.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
anbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
tarifAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -242,7 +302,7 @@ async function loadTarife() {
|
||||
|
||||
tarifSelect.innerHTML = '<option value="">Bitte wählen...</option>';
|
||||
tarife.forEach(t => {
|
||||
tarifSelect.innerHTML += `<option value="${t.id}">${t.name}</option>`;
|
||||
tarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -295,9 +355,9 @@ function displayTarifoptionen(tarifoptionen) {
|
||||
<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;">${option.name}</div>
|
||||
${option.code ? `<div style="font-size: 0.9em; color: #666; margin-bottom: 4px;">Code: ${option.code}</div>` : ''}
|
||||
${option.beschreibung ? `<div style="font-size: 0.8em; color: #555;">${option.beschreibung}</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>
|
||||
`;
|
||||
@@ -429,12 +489,17 @@ function displayUploadedFile(file) {
|
||||
const filesDiv = document.getElementById('uploadedFiles');
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'document-item';
|
||||
|
||||
fileDiv.innerHTML = `
|
||||
<span>${file.name} (${formatFileSize(file.size)})</span>
|
||||
<button type="button" onclick="removeFile('${file.name}')" class="btn-danger">Entfernen</button>
|
||||
`;
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -497,21 +562,31 @@ async function loadSchaltungen() {
|
||||
schaltungen.forEach(s => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${s.schaltungs_id}</td>
|
||||
<td>${formatDate(s.eingabe_datum)}</td>
|
||||
<td>${formatDate(s.schaltungs_datum)}</td>
|
||||
<td>${s.vorname} ${s.nachname}</td>
|
||||
<td>${s.firma || '-'}</td>
|
||||
<td>${s.rufnummer}</td>
|
||||
<td>${s.anbieter_name}</td>
|
||||
<td>${s.tarif_name}</td>
|
||||
<td>${s.hardware}</td>
|
||||
<td><button onclick="showDocuments(${s.id})" class="btn-success">Dokumente</button></td>
|
||||
<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 onclick="editSchaltung(${s.id})" class="btn-success" style="margin-right: 5px;">Bearbeiten</button>
|
||||
<button onclick="deleteSchaltung(${s.id}, '${s.schaltungs_id}', '${s.vorname} ${s.nachname}')" class="btn-danger">Löschen</button>
|
||||
<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);
|
||||
});
|
||||
|
||||
@@ -565,31 +640,38 @@ async function showDocuments(schaltungId) {
|
||||
// Format upload date
|
||||
const uploadDate = doc.uploaded_at ? formatDate(doc.uploaded_at) : 'Unbekannt';
|
||||
|
||||
// Add view button for PDFs
|
||||
const viewButton = fileExt === 'pdf'
|
||||
? `<button class="view-btn" onclick="viewPdfDocument(${doc.id}, '${doc.original_filename.replace(/'/g, "\\'")}')">👁️ Anzeigen</button>`
|
||||
: '';
|
||||
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">${doc.original_filename}</div>
|
||||
<div class="document-meta">Größe: ${fileSize} • Hochgeladen: ${uploadDate}</div>
|
||||
<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;">
|
||||
${viewButton}
|
||||
<button class="download-btn" onclick="downloadDocument(${doc.id})">
|
||||
${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" onclick="deleteDocument(${doc.id}, '${doc.original_filename.replace(/'/g, "\\'")}', ${schaltungId})"
|
||||
<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);
|
||||
});
|
||||
@@ -760,20 +842,21 @@ async function loadAnbieterAdmin() {
|
||||
}
|
||||
|
||||
anbieter.forEach(a => {
|
||||
// Format the date nicely
|
||||
const createdAt = new Date(a.created_at).toLocaleDateString('de-DE');
|
||||
|
||||
const row = `
|
||||
<tr>
|
||||
<td>${a.id}</td>
|
||||
<td>${a.name}</td>
|
||||
<td>${createdAt}</td>
|
||||
<td>
|
||||
<button onclick="deleteAnbieter(${a.id}, '${a.name}')" class="btn-danger">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
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>
|
||||
`;
|
||||
tbody.innerHTML += row;
|
||||
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) {
|
||||
@@ -807,21 +890,28 @@ async function loadTarifeAdmin() {
|
||||
|
||||
tarife.forEach(t => {
|
||||
const provision = `${(t.provision_euro || 0).toFixed(2).replace('.', ',')}€`;
|
||||
|
||||
const row = `
|
||||
<tr id="tarif-item-${t.id}">
|
||||
<td>${t.id}</td>
|
||||
<td>${t.anbieter_name || 'Unbekannt'}</td>
|
||||
<td>${t.name}</td>
|
||||
<td>${provision}</td>
|
||||
<td>${t.beschreibung || '-'}</td>
|
||||
<td>
|
||||
<button onclick="editTariff(${t.id})" class="btn-success" style="margin-right: 5px;">Bearbeiten</button>
|
||||
<button onclick="deleteTariff(${t.id}, '${t.name}')" class="btn-danger">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
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>
|
||||
`;
|
||||
tbody.innerHTML += row;
|
||||
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) {
|
||||
@@ -870,7 +960,7 @@ async function loadAnbieterForTarife() {
|
||||
|
||||
select.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
anbieter.forEach(a => {
|
||||
select.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anbieter für Tarife:', error);
|
||||
@@ -885,7 +975,7 @@ async function loadMitarbeiterAnbieter() {
|
||||
|
||||
select.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
anbieter.forEach(a => {
|
||||
select.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anbieter für Mitarbeiter:', error);
|
||||
@@ -900,7 +990,7 @@ async function loadTarifoptionAnbieter() {
|
||||
|
||||
select.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
anbieter.forEach(a => {
|
||||
select.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anbieter für Tarifoptionen:', error);
|
||||
@@ -1001,7 +1091,7 @@ async function loadTarifeForProvision() {
|
||||
const tarife = await apiRequest(`/api/tarife/${a.id}`);
|
||||
if (tarife.length > 0) {
|
||||
tarife.forEach(t => {
|
||||
provisionTarifSelect.innerHTML += `<option value="${t.id}">${a.name} - ${t.name}</option>`;
|
||||
provisionTarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(a.name + ' - ' + t.name)}</option>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1028,25 +1118,31 @@ async function loadProvisionsregelnAdmin() {
|
||||
|
||||
if (regeln.length > 0) {
|
||||
const providerDiv = document.createElement('div');
|
||||
providerDiv.innerHTML = `<h4>${a.name} - ${t.name}</h4>`;
|
||||
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>${typText} - ${r.beschreibung || 'Keine Beschreibung'} (${statusText})</span>
|
||||
<span>${escapeHtml(typText)} - ${escapeHtml(r.beschreibung || 'Keine Beschreibung')} (${escapeHtml(statusText)})</span>
|
||||
<div>
|
||||
<button onclick="toggleProvisionsregel(${r.id}, ${!r.aktiv})" class="btn-${r.aktiv ? 'danger' : 'success'}">
|
||||
<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);
|
||||
});
|
||||
}
|
||||
@@ -1144,7 +1240,7 @@ async function loadGlobalProvisionAnbieter() {
|
||||
globalProvisionAnbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
|
||||
anbieter.forEach(a => {
|
||||
globalProvisionAnbieterSelect.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
globalProvisionAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -1166,36 +1262,41 @@ async function loadGlobalProvisionsregelnAdmin() {
|
||||
|
||||
if (regeln.length > 0) {
|
||||
const providerDiv = document.createElement('div');
|
||||
providerDiv.innerHTML = `<h4>Globale Regeln für ${a.name}</h4>`;
|
||||
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';
|
||||
|
||||
// Show employee commission instead of shop commission
|
||||
|
||||
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} €`
|
||||
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>${displayText} (${statusText})</span>
|
||||
<span>${escapeHtml(displayText)} (${escapeHtml(statusText)})</span>
|
||||
<div>
|
||||
<button onclick="toggleGlobalProvisionsregel(${r.id}, ${!r.aktiv})" class="btn-${r.aktiv ? 'danger' : 'success'}">
|
||||
<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);
|
||||
});
|
||||
}
|
||||
@@ -1417,7 +1518,7 @@ async function editSchaltung(id) {
|
||||
editAnbieterSelect.innerHTML = '<option value="">Bitte wählen...</option>';
|
||||
|
||||
anbieter.forEach(a => {
|
||||
editAnbieterSelect.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
editAnbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
|
||||
// Fill modal with schaltung data
|
||||
@@ -1581,7 +1682,7 @@ async function loadTarifeForEdit() {
|
||||
|
||||
tarifSelect.innerHTML = '<option value="">Bitte wählen...</option>';
|
||||
tarife.forEach(t => {
|
||||
tarifSelect.innerHTML += `<option value="${t.id}">${t.name}</option>`;
|
||||
tarifSelect.innerHTML += `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
|
||||
});
|
||||
|
||||
// Clear tarifoptionen when anbieter changes
|
||||
@@ -1716,17 +1817,17 @@ async function loadGeloeschteSchaltungen() {
|
||||
geloeschte.forEach(s => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${s.schaltungs_id}</td>
|
||||
<td>${formatDate(s.eingabe_datum)}</td>
|
||||
<td>${formatDate(s.schaltungs_datum)}</td>
|
||||
<td>${s.vorname} ${s.nachname}</td>
|
||||
<td>${s.firma || '-'}</td>
|
||||
<td>${s.rufnummer}</td>
|
||||
<td>${s.anbieter_name}</td>
|
||||
<td>${s.tarif_name}</td>
|
||||
<td>${s.hardware}</td>
|
||||
<td><strong style="color: #e74c3c;">${s.loeschgrund}</strong></td>
|
||||
<td>${formatDateTime(s.geloescht_am)}</td>
|
||||
<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);
|
||||
});
|
||||
@@ -1775,7 +1876,7 @@ async function updateFilterDropdowns() {
|
||||
if (anbieterOverview) {
|
||||
anbieterOverview.innerHTML = '<option value="">Alle Anbieter</option>';
|
||||
anbieter.forEach(a => {
|
||||
anbieterOverview.innerHTML += `<option value="${a.name}">${a.name}</option>`;
|
||||
anbieterOverview.innerHTML += `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1784,7 +1885,7 @@ async function updateFilterDropdowns() {
|
||||
if (anbieterProvisionen) {
|
||||
anbieterProvisionen.innerHTML = '<option value="">Alle Anbieter</option>';
|
||||
anbieter.forEach(a => {
|
||||
anbieterProvisionen.innerHTML += `<option value="${a.name}">${a.name}</option>`;
|
||||
anbieterProvisionen.innerHTML += `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1808,10 +1909,10 @@ async function updateFilterDropdowns() {
|
||||
const optionValue = t.name;
|
||||
|
||||
if (tarifOverview) {
|
||||
tarifOverview.innerHTML += `<option value="${optionValue}">${optionText}</option>`;
|
||||
tarifOverview.innerHTML += `<option value="${escapeHtml(optionValue)}">${escapeHtml(optionText)}</option>`;
|
||||
}
|
||||
if (tarifProvisionen) {
|
||||
tarifProvisionen.innerHTML += `<option value="${optionValue}">${optionText}</option>`;
|
||||
tarifProvisionen.innerHTML += `<option value="${escapeHtml(optionValue)}">${escapeHtml(optionText)}</option>`;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1961,23 +2062,32 @@ function updateSchaltungenTable(schaltungen) {
|
||||
schaltungen.forEach(s => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${s.schaltungs_id}</td>
|
||||
<td>${formatDate(s.eingabe_datum)}</td>
|
||||
<td>${formatDate(s.schaltungs_datum)}</td>
|
||||
<td>${s.vorname} ${s.nachname}</td>
|
||||
<td>${s.firma || '-'}</td>
|
||||
<td>${s.rufnummer}</td>
|
||||
<td>${s.anbieter_name}</td>
|
||||
<td>${s.tarif_name}</td>
|
||||
<td>${s.hardware}</td>
|
||||
<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 onclick="showDocuments(${s.id})" class="btn-success">Dokumente</button>
|
||||
<button data-filt-action="documents" data-id="${s.id}" class="btn-success">Dokumente</button>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="editSchaltung(${s.id})" class="btn-secondary">Bearbeiten</button>
|
||||
<button onclick="deleteSchaltung(${s.id})" class="btn-danger">Löschen</button>
|
||||
<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);
|
||||
});
|
||||
}
|
||||
@@ -2010,15 +2120,15 @@ function updateProvisionenTable(schaltungen, totalShopProvision, totalMitarbeite
|
||||
|
||||
schaltungen.forEach(s => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${s.schaltungs_id}</td>
|
||||
<td>${formatDate(s.schaltungs_datum)}</td>
|
||||
<td>${s.vorname} ${s.nachname}</td>
|
||||
<td>${s.anbieter_name}</td>
|
||||
<td>${s.tarif_name}</td>
|
||||
<td><strong>${s.berechnete_shop_provision || '0.00'} €</strong></td>
|
||||
<td><strong>${s.berechnete_mitarbeiter_provision || '0.00'} €</strong></td>
|
||||
<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);
|
||||
});
|
||||
@@ -2106,7 +2216,7 @@ async function loadAnbieterForTarifoptionen() {
|
||||
const select = document.getElementById('tarifoptionAnbieter');
|
||||
select.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
anbieter.forEach(a => {
|
||||
select.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anbieter:', error);
|
||||
@@ -2136,7 +2246,7 @@ async function loadTarifeForOptions() {
|
||||
<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>${tarif.name}</span>
|
||||
<span>${escapeHtml(tarif.name)}</span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
@@ -2226,15 +2336,14 @@ async function loadTarifeForEditOptions() {
|
||||
|
||||
let checkboxHtml = '<div style="padding: 15px;">';
|
||||
tarife.forEach((tarif, index) => {
|
||||
const provisionText = tarif.provision_euro ? ` (${tarif.provision_euro}€)` : '';
|
||||
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="${tarif.name.toLowerCase()}">
|
||||
<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;">${tarif.name}</div>
|
||||
${tarif.beschreibung ? `<div style="font-size: 12px; color: #6c757d;">${tarif.beschreibung}</div>` : ''}
|
||||
${tarif.provision_euro ? `<div style="font-size: 12px; color: #28a745; font-weight: 500;">Provision: ${tarif.provision_euro}€</div>` : ''}
|
||||
<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>
|
||||
@@ -2336,7 +2445,7 @@ async function loadAnbieterForAddModal() {
|
||||
const anbieterSelect = document.getElementById('addTarifoptionAnbieter');
|
||||
anbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
anbieter.forEach(a => {
|
||||
anbieterSelect.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
anbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anbieter:', error);
|
||||
@@ -2388,13 +2497,13 @@ async function loadTarifeForAddOptions() {
|
||||
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="${tarif.name.toLowerCase()}">
|
||||
<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;">${tarif.name}</div>
|
||||
${tarif.beschreibung ? `<div style="font-size: 12px; color: #6c757d;">${tarif.beschreibung}</div>` : ''}
|
||||
${tarif.provision_euro ? `<div style="font-size: 12px; color: #28a745; font-weight: 500;">Provision: ${tarif.provision_euro}€</div>` : ''}
|
||||
<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>
|
||||
@@ -2627,37 +2736,49 @@ async function displayTarifoptionenAdmin() {
|
||||
optionen.forEach(option => {
|
||||
const provision = `${(option.provision_euro || 0).toFixed(2).replace('.', ',')}€`;
|
||||
|
||||
// Handle both old and new format
|
||||
let tarifDisplay;
|
||||
// tarifDisplayHtml is the rendered HTML; tarifPlain is the plain-text variant for the title attr.
|
||||
let tarifDisplayHtml;
|
||||
let tarifPlain;
|
||||
if (option.tarif_names) {
|
||||
// New format with multiple tarifs
|
||||
tarifDisplay = option.tarif_names || '<em style="color: #28a745;">Alle Tarife</em>';
|
||||
tarifPlain = option.tarif_names || 'Alle Tarife';
|
||||
tarifDisplayHtml = option.tarif_names
|
||||
? escapeHtml(option.tarif_names)
|
||||
: '<em style="color: #28a745;">Alle Tarife</em>';
|
||||
} else {
|
||||
// Old format with single tarif
|
||||
tarifDisplay = option.tarif_id ?
|
||||
(option.tarif_name || 'Unbekannter Tarif') :
|
||||
'<em style="color: #28a745;">Alle Tarife</em>';
|
||||
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>';
|
||||
}
|
||||
}
|
||||
|
||||
// For new format, we may not have anbieter_name directly
|
||||
const anbieterName = option.anbieter_name || 'Mehrere Anbieter';
|
||||
|
||||
const row = `
|
||||
<tr>
|
||||
<td>${option.id}</td>
|
||||
<td>${anbieterName}</td>
|
||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;" title="${tarifDisplay.replace(/<[^>]*>/g, '')}">${tarifDisplay}</td>
|
||||
<td>${option.name}</td>
|
||||
<td>${option.code || '-'}</td>
|
||||
<td>${provision}</td>
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${option.beschreibung || '-'}</td>
|
||||
<td>
|
||||
<button onclick="editTarifoption(${option.id})" class="btn-success" style="margin-right: 5px;">Bearbeiten</button>
|
||||
<button onclick="deleteTarifoption(${option.id})" class="btn-danger">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
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>
|
||||
`;
|
||||
tbody.innerHTML += row;
|
||||
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);
|
||||
@@ -2749,7 +2870,7 @@ async function editTarifoption(optionId) {
|
||||
const anbieterSelect = document.getElementById('editTarifoptionAnbieter');
|
||||
anbieterSelect.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
anbieter.forEach(a => {
|
||||
anbieterSelect.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
anbieterSelect.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
|
||||
// Try to get from new API first, fallback to old API
|
||||
@@ -2949,16 +3070,16 @@ async function loadMitarbeiterProzentsaetzeAdmin() {
|
||||
providerDiv.className = 'admin-card';
|
||||
providerDiv.innerHTML = `
|
||||
<div class="admin-card-header">
|
||||
<h4>${a.name}</h4>
|
||||
<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 {
|
||||
@@ -2967,16 +3088,19 @@ async function loadMitarbeiterProzentsaetzeAdmin() {
|
||||
item.className = 'list-item';
|
||||
item.innerHTML = `
|
||||
<div>
|
||||
<strong>${p.prozentsatz}%</strong>
|
||||
${p.beschreibung ? `- ${p.beschreibung}` : ''}
|
||||
<br><small class="text-muted">Gültig ab: ${p.gueltig_ab}</small>
|
||||
<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 onclick="deleteMitarbeiterProzentsatz(${p.id})" class="btn-danger">
|
||||
<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);
|
||||
});
|
||||
}
|
||||
@@ -2995,7 +3119,7 @@ async function loadMitarbeiterAnbieter() {
|
||||
select.innerHTML = '<option value="">Anbieter wählen...</option>';
|
||||
|
||||
anbieter.forEach(a => {
|
||||
select.innerHTML += `<option value="${a.id}">${a.name}</option>`;
|
||||
select.innerHTML += `<option value="${a.id}">${escapeHtml(a.name)}</option>`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -3081,21 +3205,21 @@ async function loadTarifProvisionsAdmin() {
|
||||
tarifDiv.className = 'admin-card';
|
||||
tarifDiv.innerHTML = `
|
||||
<div class="admin-card-header">
|
||||
<h4>${tarif.name} (${tarif.anbieter_name})</h4>
|
||||
<span class="text-muted">Basis-Provision: ${tarif.provision_euro}€</span>
|
||||
<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"
|
||||
<input type="number"
|
||||
id="provision-${tarif.id}"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value="${tarif.mitarbeiter_provision || ''}"
|
||||
value="${escapeHtml(String(tarif.mitarbeiter_provision || ''))}"
|
||||
style="width: 120px;">
|
||||
<button onclick="saveTarifProvision(${tarif.id})" class="btn-success">
|
||||
<button data-tp-action="save" data-id="${tarif.id}" class="btn-success">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
@@ -3105,6 +3229,9 @@ async function loadTarifProvisionsAdmin() {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
tarifDiv.querySelector('button[data-tp-action]').addEventListener('click', (e) => {
|
||||
saveTarifProvision(parseInt(e.currentTarget.dataset.id, 10));
|
||||
});
|
||||
container.appendChild(tarifDiv);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user