const express = require('express'); const path = require('path'); const cors = require('cors'); const multer = require('multer'); const fs = require('fs'); const { initDatabase, getDatabase, closeDatabase } = require('./database'); const app = express(); const PORT = process.env.PORT || 3000; // Initialize database initDatabase(); // Graceful shutdown handler function gracefulShutdown(signal) { console.log(`\n${signal} received, closing database connection...`); closeDatabase(); process.exit(0); } process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Middleware app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Create uploads directory if it doesn't exist const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir); } // Allowed file types for uploads const ALLOWED_FILE_TYPES = { 'application/pdf': ['.pdf'], 'image/jpeg': ['.jpg', '.jpeg'], 'image/png': ['.png'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'] }; // File filter for multer const fileFilter = (req, file, cb) => { const allowedExtensions = Object.values(ALLOWED_FILE_TYPES).flat(); const ext = path.extname(file.originalname).toLowerCase(); if (allowedExtensions.includes(ext) && ALLOWED_FILE_TYPES[file.mimetype]) { cb(null, true); } else { cb(new Error(`Dateityp nicht erlaubt: ${ext}. Erlaubt sind: PDF, JPG, PNG, DOC, DOCX`), false); } }; // Configure multer for file uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, fileFilter: fileFilter, limits: { fileSize: 10 * 1024 * 1024 // 10MB limit } }); // Validation helper functions function validateDate(dateString) { const date = new Date(dateString); return dateString && !isNaN(date.getTime()) && /^\d{4}-\d{2}-\d{2}$/.test(dateString); } function validateHardware(hardware) { // Hardware should be 15 characters if provided return !hardware || hardware.length === 15; } function validateAmount(amount) { // Amount should be positive or zero return amount === null || amount === undefined || amount >= 0; } function validatePercentage(percentage) { // Percentage should be between 0 and 100 return percentage === null || percentage === undefined || (percentage >= 0 && percentage <= 100); } // API Routes // Get all providers app.get('/api/anbieter', (req, res) => { const db = getDatabase(); db.all('SELECT * FROM anbieter ORDER BY name', (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add new provider app.post('/api/anbieter', (req, res) => { const { name } = req.body; const db = getDatabase(); db.run('INSERT INTO anbieter (name) VALUES (?)', [name], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ id: this.lastID, name }); }); }); // Delete provider app.delete('/api/anbieter/:id', (req, res) => { const { id } = req.params; const db = getDatabase(); // Check if provider has tariffs db.get('SELECT COUNT(*) as tariff_count FROM tarife WHERE anbieter_id = ?', [id], (err, tariffeRow) => { if (err) { res.status(500).json({ error: err.message }); return; } // Check if provider has schaltungen db.get('SELECT COUNT(*) as schaltung_count FROM schaltungen WHERE anbieter_id = ?', [id], (err, schaltungenRow) => { if (err) { res.status(500).json({ error: err.message }); return; } // Check if provider has global commission rules // Only check critical dependencies (tariffs and schaltungen) const criticalUsages = tariffeRow.tariff_count + schaltungenRow.schaltung_count; if (criticalUsages > 0) { const reasons = []; if (tariffeRow.tariff_count > 0) reasons.push(`${tariffeRow.tariff_count} Tarif(e)`); if (schaltungenRow.schaltung_count > 0) reasons.push(`${schaltungenRow.schaltung_count} Schaltung(en)`); res.status(400).json({ error: `Anbieter kann nicht gelöscht werden: ${reasons.join(', ')} verwenden diesen Anbieter` }); return; } // First delete all associated commission rules (these can be safely deleted) db.run('DELETE FROM anbieter_provisionsregeln WHERE anbieter_id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } const deletedRules = this.changes; // Then delete the provider db.run('DELETE FROM anbieter WHERE id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Anbieter nicht gefunden' }); return; } let message = 'Anbieter erfolgreich gelöscht'; if (deletedRules > 0) { message += ` (${deletedRules} Provisionsregel(n) wurden mit gelöscht)`; } res.json({ message }); }); }); }); }); }); // Get tariffs by provider // Get all tarife with anbieter names app.get('/api/tarife', (req, res) => { const db = getDatabase(); const query = ` SELECT t.*, a.name as anbieter_name FROM tarife t JOIN anbieter a ON t.anbieter_id = a.id ORDER BY a.name, t.name `; db.all(query, [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); app.get('/api/tarife/:anbieter_id', (req, res) => { const { anbieter_id } = req.params; const db = getDatabase(); db.all('SELECT * FROM tarife WHERE anbieter_id = ? ORDER BY name', [anbieter_id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Get single tariff by ID app.get('/api/tarif/:id', (req, res) => { const { id } = req.params; const db = getDatabase(); db.get('SELECT * FROM tarife WHERE id = ?', [id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!row) { res.status(404).json({ error: 'Tarif nicht gefunden' }); return; } res.json(row); }); }); // Add new tariff - simplified app.post('/api/tarife', (req, res) => { const { name, anbieter_id, provision_euro, beschreibung } = req.body; // Input validation if (!name || !name.trim()) { return res.status(400).json({ error: 'Tarifname ist erforderlich' }); } if (!anbieter_id) { return res.status(400).json({ error: 'Anbieter ist erforderlich' }); } if (!validateAmount(provision_euro)) { return res.status(400).json({ error: 'Provision muss eine positive Zahl sein' }); } const db = getDatabase(); const query = `INSERT INTO tarife (name, anbieter_id, provision_euro, beschreibung) VALUES (?, ?, ?, ?)`; db.run(query, [name, anbieter_id, provision_euro || 0, beschreibung || null], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ id: this.lastID, name, anbieter_id, provision_euro: provision_euro || 0, beschreibung }); }); }); // Update tariff - simplified app.put('/api/tarife/:id', (req, res) => { const { id } = req.params; const { name, provision_euro, beschreibung } = req.body; const db = getDatabase(); if (!name) { return res.status(400).json({ error: 'Tarif-Name ist erforderlich' }); } const query = `UPDATE tarife SET name = ?, provision_euro = ?, beschreibung = ? WHERE id = ?`; db.run(query, [name, provision_euro || 0, beschreibung || null, id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Tarif nicht gefunden' }); return; } res.json({ message: 'Tarif erfolgreich aktualisiert' }); }); }); // Delete tariff app.delete('/api/tarife/:id', (req, res) => { const { id } = req.params; const db = getDatabase(); // Fast check if tariff is used in any schaltungen db.get('SELECT EXISTS(SELECT 1 FROM schaltungen WHERE tarif_id = ?) as has_schaltungen', [id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } if (row.has_schaltungen) { res.status(400).json({ error: `Tarif kann nicht gelöscht werden: Tarif wird in Schaltungen verwendet` }); return; } // Delete the tariff db.run('DELETE FROM tarife WHERE id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Tarif nicht gefunden' }); return; } res.json({ message: 'Tarif erfolgreich gelöscht' }); }); }); }); // Get all schaltungen with related data app.get('/api/schaltungen', (req, res) => { const db = getDatabase(); const query = ` SELECT s.*, a.name as anbieter_name, t.name as tarif_name FROM schaltungen s JOIN anbieter a ON s.anbieter_id = a.id JOIN tarife t ON s.tarif_id = t.id ORDER BY s.created_at DESC `; db.all(query, (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add new schaltung app.post('/api/schaltungen', (req, res) => { const { schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer, anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto } = req.body; // Input validation if (!schaltungs_id || !schaltungs_id.trim()) { return res.status(400).json({ error: 'Schaltungs-ID ist erforderlich' }); } if (!validateDate(eingabe_datum)) { return res.status(400).json({ error: 'Ungültiges Eingabe-Datum (Format: YYYY-MM-DD)' }); } if (!validateDate(schaltungs_datum)) { return res.status(400).json({ error: 'Ungültiges Schaltungs-Datum (Format: YYYY-MM-DD)' }); } if (!vorname || !vorname.trim()) { return res.status(400).json({ error: 'Vorname ist erforderlich' }); } if (!nachname || !nachname.trim()) { return res.status(400).json({ error: 'Nachname ist erforderlich' }); } if (!rufnummer || !rufnummer.trim()) { return res.status(400).json({ error: 'Rufnummer ist erforderlich' }); } if (!anbieter_id) { return res.status(400).json({ error: 'Anbieter ist erforderlich' }); } if (!tarif_id) { return res.status(400).json({ error: 'Tarif ist erforderlich' }); } if (!validateHardware(hardware)) { return res.status(400).json({ error: 'Hardware muss genau 15 Zeichen lang sein' }); } const db = getDatabase(); // Validation: if hardware is provided, hardware_ek and kundenverkaufspreis_netto must be provided if (hardware && hardware.trim() !== '') { if (!validateAmount(hardware_ek)) { return res.status(400).json({ error: 'Hardware-EK muss eine positive Zahl sein' }); } if (!validateAmount(kundenverkaufspreis_netto)) { return res.status(400).json({ error: 'Kundenverkaufspreis muss eine positive Zahl sein' }); } } const query = ` INSERT INTO schaltungen (schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer, anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; db.run(query, [ schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer, anbieter_id, tarif_id, hardware, hardware_ek || null, kundenverkaufspreis_netto || null ], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ id: this.lastID, message: 'Schaltung erfolgreich erstellt' }); }); }); // Update schaltung app.put('/api/schaltungen/:id', (req, res) => { const { id } = req.params; const { schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer, anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto, tarifoptionen = [] } = req.body; const db = getDatabase(); // Begin transaction db.serialize(() => { db.run('BEGIN TRANSACTION'); // Update schaltung const updateQuery = ` UPDATE schaltungen SET schaltungs_id = ?, eingabe_datum = ?, schaltungs_datum = ?, vorname = ?, nachname = ?, firma = ?, rufnummer = ?, anbieter_id = ?, tarif_id = ?, hardware = ?, hardware_ek = ?, kundenverkaufspreis_netto = ? WHERE id = ? `; db.run(updateQuery, [ schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer, anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto, id ], function(err) { if (err) { db.run('ROLLBACK'); res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { db.run('ROLLBACK'); res.status(404).json({ error: 'Schaltung nicht gefunden' }); return; } // Delete existing tarifoptionen db.run('DELETE FROM schaltungen_tarifoptionen WHERE schaltung_id = ?', [id], (err) => { if (err) { db.run('ROLLBACK'); res.status(500).json({ error: err.message }); return; } // Insert new tarifoptionen if (tarifoptionen.length > 0) { const insertTarifoptionQuery = 'INSERT INTO schaltungen_tarifoptionen (schaltung_id, tarifoption_id) VALUES (?, ?)'; let insertCount = 0; tarifoptionen.forEach(tarifoptionId => { db.run(insertTarifoptionQuery, [id, tarifoptionId], (err) => { if (err) { db.run('ROLLBACK'); res.status(500).json({ error: err.message }); return; } insertCount++; if (insertCount === tarifoptionen.length) { // All tarifoptionen inserted successfully db.run('COMMIT'); res.json({ message: 'Schaltung erfolgreich aktualisiert' }); } }); }); } else { // No tarifoptionen to insert db.run('COMMIT'); res.json({ message: 'Schaltung erfolgreich aktualisiert' }); } }); }); }); }); // Get tarifoptionen for a specific schaltung app.get('/api/schaltungen/:id/tarifoptionen', (req, res) => { const { id } = req.params; const db = getDatabase(); const query = ` SELECT st.tarifoption_id, tarifoptionen.name, tarifoptionen.provision_euro FROM schaltungen_tarifoptionen st JOIN tarifoptionen ON st.tarifoption_id = tarifoptionen.id WHERE st.schaltung_id = ? `; db.all(query, [id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Delete schaltung with reason tracking app.delete('/api/schaltungen/:id', (req, res) => { const { id } = req.params; const { loeschgrund } = req.body; if (!loeschgrund || loeschgrund.trim().length === 0) { return res.status(400).json({ error: 'Löschgrund ist erforderlich' }); } const db = getDatabase(); // Get schaltung details before deleting const getQuery = ` SELECT s.*, a.name as anbieter_name, t.name as tarif_name FROM schaltungen s JOIN anbieter a ON s.anbieter_id = a.id JOIN tarife t ON s.tarif_id = t.id WHERE s.id = ? `; db.get(getQuery, [id], (err, schaltung) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!schaltung) { res.status(404).json({ error: 'Schaltung nicht gefunden' }); return; } // Insert into deleted schaltungen table const insertQuery = ` INSERT INTO geloeschte_schaltungen ( original_id, schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer, anbieter_id, tarif_id, hardware, anbieter_name, tarif_name, loeschgrund ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; db.run(insertQuery, [ schaltung.id, schaltung.schaltungs_id, schaltung.eingabe_datum, schaltung.schaltungs_datum, schaltung.vorname, schaltung.nachname, schaltung.firma, schaltung.rufnummer, schaltung.anbieter_id, schaltung.tarif_id, schaltung.hardware, schaltung.anbieter_name, schaltung.tarif_name, loeschgrund.trim() ], function(insertErr) { if (insertErr) { res.status(500).json({ error: insertErr.message }); return; } // Get all documents for this schaltung before deleting db.all('SELECT filepath FROM dokumente WHERE schaltung_id = ?', [id], (err, documents) => { if (err) { console.error('Error fetching documents for deletion:', err.message); } // Delete physical files from filesystem if (documents && documents.length > 0) { documents.forEach(doc => { const filePath = path.join(__dirname, doc.filepath); fs.unlink(filePath, (unlinkErr) => { if (unlinkErr) { console.error(`Error deleting file ${filePath}:`, unlinkErr.message); } else { console.log(`Deleted file: ${filePath}`); } }); }); } // Delete documents from database (will CASCADE from schaltungen deletion) // Delete from schaltungen table (this will CASCADE delete dokumente and schaltungen_tarifoptionen) db.run('DELETE FROM schaltungen WHERE id = ?', [id], function(deleteErr) { if (deleteErr) { res.status(500).json({ error: deleteErr.message }); return; } res.json({ message: 'Schaltung erfolgreich gelöscht und archiviert', loeschgrund: loeschgrund.trim(), deleted_files: documents ? documents.length : 0 }); }); }); }); }); }); // Get deleted schaltungen app.get('/api/geloeschte-schaltungen', (req, res) => { const db = getDatabase(); db.all('SELECT * FROM geloeschte_schaltungen ORDER BY geloescht_am DESC', (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Upload document for schaltung app.post('/api/schaltungen/:id/dokumente', upload.single('document'), (req, res) => { const schaltung_id = req.params.id; const file = req.file; if (!file) { return res.status(400).json({ error: 'Keine Datei hochgeladen' }); } const db = getDatabase(); // First verify schaltung exists db.get('SELECT id FROM schaltungen WHERE id = ?', [schaltung_id], (err, schaltung) => { if (err) { // Cleanup uploaded file fs.unlink(file.path, (unlinkErr) => { if (unlinkErr) console.error('Error cleaning up file:', unlinkErr); }); return res.status(500).json({ error: err.message }); } if (!schaltung) { // Cleanup uploaded file fs.unlink(file.path, (unlinkErr) => { if (unlinkErr) console.error('Error cleaning up file:', unlinkErr); }); return res.status(404).json({ error: 'Schaltung nicht gefunden' }); } const query = ` INSERT INTO dokumente (schaltung_id, filename, original_filename, filepath, filesize, mimetype) VALUES (?, ?, ?, ?, ?, ?) `; db.run(query, [ schaltung_id, file.filename, file.originalname, file.path, file.size, file.mimetype ], function(err) { if (err) { // Cleanup uploaded file on database error fs.unlink(file.path, (unlinkErr) => { if (unlinkErr) console.error('Error cleaning up file:', unlinkErr); }); return res.status(500).json({ error: err.message }); } res.json({ id: this.lastID, message: 'Dokument erfolgreich hochgeladen', filename: file.originalname }); }); }); }); // Error handler for multer app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Datei zu groß. Maximum: 10MB' }); } return res.status(400).json({ error: err.message }); } else if (err) { return res.status(400).json({ error: err.message }); } next(); }); // Get documents for schaltung app.get('/api/schaltungen/:id/dokumente', (req, res) => { const schaltung_id = req.params.id; const db = getDatabase(); db.all('SELECT * FROM dokumente WHERE schaltung_id = ?', [schaltung_id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Download document app.get('/api/dokumente/:id/download', (req, res) => { const document_id = req.params.id; const db = getDatabase(); db.get('SELECT * FROM dokumente WHERE id = ?', [document_id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!row) { res.status(404).json({ error: 'Dokument nicht gefunden' }); return; } const filepath = path.join(__dirname, row.filepath); res.download(filepath, row.original_filename); }); }); // Delete document app.delete('/api/dokumente/:id', (req, res) => { const document_id = req.params.id; const db = getDatabase(); // First get the document info to delete the file db.get('SELECT * FROM dokumente WHERE id = ?', [document_id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!row) { res.status(404).json({ error: 'Dokument nicht gefunden' }); return; } // Delete from database db.run('DELETE FROM dokumente WHERE id = ?', [document_id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } // Delete physical file const filepath = path.join(__dirname, row.filepath); fs.unlink(filepath, (unlinkErr) => { if (unlinkErr) { console.error('Fehler beim Löschen der Datei:', unlinkErr); // Continue anyway as database record is already deleted } res.json({ message: 'Dokument erfolgreich gelöscht' }); }); }); }); }); // Global Provider Commission Rules API // Get global commission rules for provider app.get('/api/anbieter/:id/provisionsregeln', (req, res) => { const { id } = req.params; const db = getDatabase(); const query = ` SELECT apr.*, a.name as anbieter_name FROM anbieter_provisionsregeln apr JOIN anbieter a ON apr.anbieter_id = a.id WHERE apr.anbieter_id = ? AND apr.aktiv = 1 ORDER BY apr.gueltig_ab DESC `; db.all(query, [id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add global provider commission rule app.post('/api/anbieter-provisionsregeln', (req, res) => { const { anbieter_id, provision_typ, betrag, beschreibung, gueltig_ab, verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung } = req.body; const db = getDatabase(); // Validate provision type and amount if (!['fix', 'prozent'].includes(provision_typ)) { return res.status(400).json({ error: 'Ungültiger Provisionstyp' }); } if (provision_typ === 'prozent' && (betrag < 0 || betrag > 100)) { return res.status(400).json({ error: 'Prozentual muss zwischen 0 und 100 sein' }); } if (provision_typ === 'fix' && betrag < 0) { return res.status(400).json({ error: 'Fixer Betrag kann nicht negativ sein' }); } const query = ` INSERT INTO anbieter_provisionsregeln ( anbieter_id, provision_typ, betrag, beschreibung, gueltig_ab, verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; db.run(query, [ anbieter_id, provision_typ, betrag, beschreibung, gueltig_ab || new Date().toISOString().split('T')[0], verkaeufer_provision_typ || null, verkaeufer_provision_betrag || null, verkaeufer_provision_beschreibung || null ], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ id: this.lastID, message: 'Globale Provisionsregel erfolgreich erstellt' }); }); }); // Update global provider commission rule app.put('/api/anbieter-provisionsregeln/:id', (req, res) => { const { id } = req.params; const { provision_typ, betrag, beschreibung, gueltig_ab, aktiv, verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung } = req.body; const db = getDatabase(); // First get the current rule to preserve existing values db.get('SELECT * FROM anbieter_provisionsregeln WHERE id = ?', [id], (err, current) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!current) { res.status(404).json({ error: 'Globale Provisionsregel nicht gefunden' }); return; } const updateQuery = ` UPDATE anbieter_provisionsregeln SET provision_typ = ?, betrag = ?, beschreibung = ?, gueltig_ab = ?, aktiv = ?, verkaeufer_provision_typ = ?, verkaeufer_provision_betrag = ?, verkaeufer_provision_beschreibung = ? WHERE id = ? `; db.run(updateQuery, [ provision_typ !== undefined ? provision_typ : current.provision_typ, betrag !== undefined ? betrag : current.betrag, beschreibung !== undefined ? beschreibung : current.beschreibung, gueltig_ab !== undefined ? gueltig_ab : current.gueltig_ab, aktiv !== undefined ? aktiv : current.aktiv, verkaeufer_provision_typ !== undefined ? verkaeufer_provision_typ : current.verkaeufer_provision_typ, verkaeufer_provision_betrag !== undefined ? verkaeufer_provision_betrag : current.verkaeufer_provision_betrag, verkaeufer_provision_beschreibung !== undefined ? verkaeufer_provision_beschreibung : current.verkaeufer_provision_beschreibung, id ], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ message: 'Globale Provisionsregel erfolgreich aktualisiert' }); }); }); }); // Commission Rules API // Get commission rules for tariff app.get('/api/tarife/:id/provisionsregeln', (req, res) => { const { id } = req.params; const db = getDatabase(); const query = ` SELECT pr.*, t.name as tarif_name, a.name as anbieter_name FROM provisionsregeln pr JOIN tarife t ON pr.tarif_id = t.id JOIN anbieter a ON t.anbieter_id = a.id WHERE pr.tarif_id = ? AND pr.aktiv = 1 ORDER BY pr.gueltig_ab DESC `; db.all(query, [id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add commission rule app.post('/api/provisionsregeln', (req, res) => { const { tarif_id, provision_typ, betrag, beschreibung, gueltig_ab } = req.body; const db = getDatabase(); // Validate provision type and amount if (!['fix', 'prozent'].includes(provision_typ)) { return res.status(400).json({ error: 'Ungültiger Provisionstyp' }); } if (provision_typ === 'prozent' && (betrag < 0 || betrag > 100)) { return res.status(400).json({ error: 'Prozentual muss zwischen 0 und 100 sein' }); } if (provision_typ === 'fix' && betrag < 0) { return res.status(400).json({ error: 'Fixer Betrag kann nicht negativ sein' }); } const query = ` INSERT INTO provisionsregeln (tarif_id, provision_typ, betrag, beschreibung, gueltig_ab) VALUES (?, ?, ?, ?, ?) `; db.run(query, [tarif_id, provision_typ, betrag, beschreibung, gueltig_ab || new Date().toISOString().split('T')[0]], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ id: this.lastID, message: 'Provisionsregel erfolgreich erstellt' }); }); }); // Update commission rule app.put('/api/provisionsregeln/:id', (req, res) => { const { id } = req.params; const { provision_typ, betrag, beschreibung, gueltig_ab, aktiv } = req.body; const db = getDatabase(); // First get the current rule to preserve existing values db.get('SELECT * FROM provisionsregeln WHERE id = ?', [id], (err, current) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!current) { res.status(404).json({ error: 'Provisionsregel nicht gefunden' }); return; } // Use current values as defaults const updateQuery = ` UPDATE provisionsregeln SET provision_typ = ?, betrag = ?, beschreibung = ?, gueltig_ab = ?, aktiv = ? WHERE id = ? `; db.run(updateQuery, [ provision_typ !== undefined ? provision_typ : current.provision_typ, betrag !== undefined ? betrag : current.betrag, beschreibung !== undefined ? beschreibung : current.beschreibung, gueltig_ab !== undefined ? gueltig_ab : current.gueltig_ab, aktiv !== undefined ? aktiv : current.aktiv, id ], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ message: 'Provisionsregel erfolgreich aktualisiert' }); }); }); }); // Get commission calculation for schaltung app.get('/api/schaltungen/:id/provision', (req, res) => { const { id } = req.params; const db = getDatabase(); const query = ` SELECT s.*, t.name as tarif_name, t.provision_euro as tarif_provision_euro, t.beschreibung as tarif_beschreibung, a.name as anbieter_name, mp.prozentsatz as mitarbeiter_prozentsatz FROM schaltungen s JOIN tarife t ON s.tarif_id = t.id JOIN anbieter a ON s.anbieter_id = a.id LEFT JOIN mitarbeiter_prozentsaetze mp ON a.id = mp.anbieter_id AND mp.gueltig_ab <= s.schaltungs_datum WHERE s.id = ? ORDER BY mp.gueltig_ab DESC LIMIT 1 `; db.get(query, [id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!row) { res.status(404).json({ error: 'Schaltung nicht gefunden' }); return; } // Get tarifoptionen for this schaltung const tarifoptionenQuery = ` SELECT to.* FROM tarifoptionen to JOIN schaltungen_tarifoptionen st ON to.id = st.tarifoption_id WHERE st.schaltung_id = ? `; db.all(tarifoptionenQuery, [id], (err, tarifoptionen) => { if (err) { res.status(500).json({ error: err.message }); return; } // Calculate shop commission with hierarchy let shop_provision_typ = null; let shop_provision_betrag = null; let shop_provision_beschreibung = null; let berechnete_shop_provision = 0; let shop_quelle = 'Keine Shop-Provision definiert'; // Priority 1: Direct tariff commission if (row.tarif_provision_typ && row.tarif_provision_betrag) { shop_provision_typ = row.tarif_provision_typ; shop_provision_betrag = row.tarif_provision_betrag; shop_provision_beschreibung = row.tarif_provision_beschreibung; shop_quelle = 'Direkte Tarif-Provision'; } // Priority 2: Tariff-specific rule else if (row.regel_provision_typ && row.regel_provision_betrag) { shop_provision_typ = row.regel_provision_typ; shop_provision_betrag = row.regel_provision_betrag; shop_provision_beschreibung = row.regel_provision_beschreibung; shop_quelle = 'Tarif-spezifische Regel'; } // Priority 3: Global provider rule as fallback else if (row.global_provision_typ && row.global_provision_betrag) { shop_provision_typ = row.global_provision_typ; shop_provision_betrag = row.global_provision_betrag; shop_provision_beschreibung = row.global_provision_beschreibung; shop_quelle = 'Globale Anbieter-Regel'; } // Calculate shop commission if (shop_provision_typ && shop_provision_betrag) { if (shop_provision_typ === 'fix') { berechnete_shop_provision = parseFloat(shop_provision_betrag); } else if (shop_provision_typ === 'prozent') { const basisBetrag = 100; // This could come from tariff table berechnete_shop_provision = (parseFloat(shop_provision_betrag) / 100) * basisBetrag; } } // Add tarifoptionen to shop commission let tarifoptionen_provision = 0; let verwendete_tarifoptionen = []; if (tarifoptionen && tarifoptionen.length > 0) { tarifoptionen.forEach(option => { if (option.provision_betrag && option.provision_betrag > 0) { tarifoptionen_provision += parseFloat(option.provision_betrag); verwendete_tarifoptionen.push(option.name); } }); } // Add tarifoptionen to total shop commission berechnete_shop_provision += tarifoptionen_provision; // Update shop commission description if tarifoptionen were added if (tarifoptionen_provision > 0) { const tarifoptionen_text = verwendete_tarifoptionen.join(', '); shop_provision_beschreibung = shop_provision_beschreibung ? `${shop_provision_beschreibung} + Tarifoptionen: ${tarifoptionen_text}` : `Tarifoptionen: ${tarifoptionen_text}`; } // Calculate employee commission with hierarchy let mitarbeiter_provision_typ = null; let mitarbeiter_provision_betrag = null; let mitarbeiter_provision_beschreibung = null; let berechnete_mitarbeiter_provision = 0; let mitarbeiter_quelle = 'Keine Mitarbeiter-Provision definiert'; // Priority 1: Direct tariff employee commission if (row.tarif_verkaeufer_provision_typ && row.tarif_verkaeufer_provision_betrag) { mitarbeiter_provision_typ = row.tarif_verkaeufer_provision_typ; mitarbeiter_provision_betrag = row.tarif_verkaeufer_provision_betrag; mitarbeiter_provision_beschreibung = row.tarif_verkaeufer_provision_beschreibung; mitarbeiter_quelle = 'Direkte Tarif-Verkäufer-Provision'; } // Priority 2: Tariff-specific rule employee commission else if (row.regel_verkaeufer_provision_typ && row.regel_verkaeufer_provision_betrag) { mitarbeiter_provision_typ = row.regel_verkaeufer_provision_typ; mitarbeiter_provision_betrag = row.regel_verkaeufer_provision_betrag; mitarbeiter_provision_beschreibung = row.regel_verkaeufer_provision_beschreibung; mitarbeiter_quelle = 'Tarif-spezifische Verkäufer-Regel'; } // Priority 3: Global provider rule employee commission else if (row.global_verkaeufer_provision_typ && row.global_verkaeufer_provision_betrag) { mitarbeiter_provision_typ = row.global_verkaeufer_provision_typ; mitarbeiter_provision_betrag = row.global_verkaeufer_provision_betrag; mitarbeiter_provision_beschreibung = row.global_verkaeufer_provision_beschreibung; mitarbeiter_quelle = 'Globale Anbieter-Verkäufer-Regel'; } // Calculate employee commission if (mitarbeiter_provision_typ && mitarbeiter_provision_betrag) { if (mitarbeiter_provision_typ === 'fix') { berechnete_mitarbeiter_provision = parseFloat(mitarbeiter_provision_betrag); } else if (mitarbeiter_provision_typ === 'prozent') { // If percentage, calculate based on shop commission berechnete_mitarbeiter_provision = (parseFloat(mitarbeiter_provision_betrag) / 100) * berechnete_shop_provision; } } res.json({ id: row.id, schaltungs_id: row.schaltungs_id, schaltungs_datum: row.schaltungs_datum, vorname: row.vorname, nachname: row.nachname, firma: row.firma, tarif_name: row.tarif_name, anbieter_name: row.anbieter_name, // Shop commission shop_provision_typ, shop_provision_betrag, shop_provision_beschreibung, berechnete_shop_provision: berechnete_shop_provision.toFixed(2), shop_quelle, // Employee commission mitarbeiter_provision_typ, mitarbeiter_provision_betrag, mitarbeiter_provision_beschreibung, berechnete_mitarbeiter_provision: berechnete_mitarbeiter_provision.toFixed(2), mitarbeiter_quelle }); }); }); }); // Get commission overview for all schaltungen with hierarchy app.get('/api/provisionen', async (req, res) => { const db = getDatabase(); // Simplified query for new commission model - use subquery to get latest valid percentage const query = ` SELECT s.id, s.schaltungs_id, s.eingabe_datum, s.schaltungs_datum, s.vorname, s.nachname, s.firma, s.hardware, s.hardware_ek, s.kundenverkaufspreis_netto, t.name as tarif_name, t.provision_euro as tarif_provision_euro, a.name as anbieter_name, a.id as anbieter_id, ( SELECT mp.prozentsatz FROM mitarbeiter_prozentsaetze mp WHERE mp.anbieter_id = a.id AND mp.gueltig_ab <= s.schaltungs_datum ORDER BY mp.gueltig_ab DESC LIMIT 1 ) as mitarbeiter_prozentsatz FROM schaltungen s JOIN tarife t ON s.tarif_id = t.id JOIN anbieter a ON s.anbieter_id = a.id ORDER BY s.schaltungs_datum DESC `; try { const rows = await new Promise((resolve, reject) => { db.all(query, [], (err, data) => { if (err) reject(err); else resolve(data); }); }); // Get tarifoptionen for all schaltungen first const schaltungIds = rows.map(row => row.id); let tarifoptionenMap = {}; if (schaltungIds.length > 0) { const tarifoptionenQuery = ` SELECT st.schaltung_id, tarifoptionen.provision_euro FROM schaltungen_tarifoptionen st JOIN tarifoptionen ON st.tarifoption_id = tarifoptionen.id WHERE st.schaltung_id IN (${schaltungIds.map(() => '?').join(',')}) `; try { const tarifoptionenRows = await new Promise((resolve, reject) => { db.all(tarifoptionenQuery, schaltungIds, (err, tarifoptionenData) => { if (err) reject(err); else resolve(tarifoptionenData); }); }); // Group tarifoptionen by schaltung_id tarifoptionenRows.forEach(opt => { if (!tarifoptionenMap[opt.schaltung_id]) { tarifoptionenMap[opt.schaltung_id] = []; } tarifoptionenMap[opt.schaltung_id].push(parseFloat(opt.provision_euro) || 0); }); } catch (error) { console.error('Error loading tarifoptionen:', error); } } // Process new commission calculation with hardware pricing and tarifoptionen const processedRows = rows.map(row => { // Base shop commission = Tarif provision let shop_provision = parseFloat(row.tarif_provision_euro) || 0; // Add tarifoptionen provision const tarifoptionen = tarifoptionenMap[row.id] || []; const tarifoptionen_provision = tarifoptionen.reduce((sum, opt) => sum + opt, 0); shop_provision += tarifoptionen_provision; // Enhanced calculation: Tarifprovision + Tarifoptionsprovision - Hardware-EK + Kundenverkaufspreis if (row.hardware && row.hardware.trim() !== '') { const hardware_ek = parseFloat(row.hardware_ek) || 0; const kundenverkaufspreis = parseFloat(row.kundenverkaufspreis_netto) || 0; // Apply the new formula: Tarifprovision + Tarifoptionen - Hardware-EK + Kundenverkaufspreis shop_provision = shop_provision - hardware_ek + kundenverkaufspreis; } // Employee commission = Shop provision * Employee percentage const mitarbeiter_prozentsatz = parseFloat(row.mitarbeiter_prozentsatz) || 0; const mitarbeiter_provision = shop_provision * (mitarbeiter_prozentsatz / 100); return { id: row.id, schaltungs_id: row.schaltungs_id, eingabe_datum: row.eingabe_datum, schaltungs_datum: row.schaltungs_datum, vorname: row.vorname, nachname: row.nachname, firma: row.firma, hardware: row.hardware, hardware_ek: row.hardware_ek, kundenverkaufspreis_netto: row.kundenverkaufspreis_netto, tarif_name: row.tarif_name, anbieter_name: row.anbieter_name, // Legacy fields for frontend compatibility provision_typ: 'fix', provision_betrag: shop_provision, provision_beschreibung: 'Neue einfache Berechnung', berechnete_provision: shop_provision.toFixed(2), quelle: 'Tarif-Provision', // New fields shop_provision_euro: shop_provision, berechnete_shop_provision: shop_provision.toFixed(2), mitarbeiter_prozentsatz: mitarbeiter_prozentsatz, berechnete_mitarbeiter_provision: mitarbeiter_provision.toFixed(2), mitarbeiter_quelle: `${mitarbeiter_prozentsatz}% von ${shop_provision}€` }; }); // Calculate totals const totalShopProvision = processedRows.reduce((sum, row) => { return sum + parseFloat(row.berechnete_shop_provision); }, 0); const totalMitarbeiterProvision = processedRows.reduce((sum, row) => { return sum + parseFloat(row.berechnete_mitarbeiter_provision); }, 0); res.json({ schaltungen: processedRows, total_provision: totalShopProvision.toFixed(2), // Legacy field total_shop_provision: totalShopProvision.toFixed(2), total_mitarbeiter_provision: totalMitarbeiterProvision.toFixed(2), total_gesamt_provision: (totalShopProvision + totalMitarbeiterProvision).toFixed(2) }); } catch (error) { console.error('Error in provisions API:', error); res.status(500).json({ error: error.message }); } }); // Tarifoptionen API // Get all tarifoptionen with anbieter and tarif names for admin app.get('/api/tarifoptionen', (req, res) => { const db = getDatabase(); const query = ` SELECT t.*, a.name as anbieter_name, ta.name as tarif_name FROM tarifoptionen t JOIN anbieter a ON t.anbieter_id = a.id LEFT JOIN tarife ta ON t.tarif_id = ta.id WHERE t.aktiv = 1 ORDER BY a.name, t.tarif_id IS NULL DESC, COALESCE(ta.name, ''), t.name `; console.log('Executing tarifoptionen query...'); db.all(query, (err, rows) => { if (err) { console.error('Tarifoptionen API error:', err.message); res.status(500).json({ error: err.message }); return; } console.log(`Tarifoptionen API: returning ${rows.length} results`); res.json(rows); }); }); // Get tarifoptionen by tariff (includes both tariff-specific and anbieter-wide options) app.get('/api/tarife/:tarif_id/tarifoptionen', (req, res) => { const { tarif_id } = req.params; const db = getDatabase(); // Get all tarifoptionen for this tariff from the old table // (includes both manually created and auto-synced from new structure) const query = ` SELECT * FROM tarifoptionen WHERE tarif_id = ? AND aktiv = 1 ORDER BY name `; db.all(query, [tarif_id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add new tarifoption app.post('/api/tarifoptionen', (req, res) => { const { anbieter_id, tarif_id, name, code, provision_euro, beschreibung } = req.body; // Validate that either anbieter_id is provided (for anbieter-wide) or tarif_id (for tariff-specific) if (!anbieter_id && !tarif_id) { return res.status(400).json({ error: 'Entweder Anbieter oder Tarif muss angegeben werden' }); } const db = getDatabase(); // If tarif_id is provided, get anbieter_id from tariff if (tarif_id && !anbieter_id) { db.get('SELECT anbieter_id FROM tarife WHERE id = ?', [tarif_id], (err, tarif) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!tarif) { res.status(404).json({ error: 'Tarif nicht gefunden' }); return; } insertTarifoption(db, res, tarif.anbieter_id, tarif_id, name, code, provision_euro, beschreibung); }); } else { // Direct anbieter-wide option insertTarifoption(db, res, anbieter_id, tarif_id || null, name, code, provision_euro, beschreibung); } }); function insertTarifoption(db, res, anbieter_id, tarif_id, name, code, provision_euro, beschreibung) { const query = ` INSERT INTO tarifoptionen ( anbieter_id, tarif_id, name, code, provision_euro, beschreibung ) VALUES (?, ?, ?, ?, ?, ?) `; db.run(query, [ anbieter_id, tarif_id, name, code || null, provision_euro || 0, beschreibung || null ], function(err) { if (err) { if (err.message.includes('UNIQUE constraint failed')) { const scope = tarif_id ? 'für diesen Tarif' : 'für diesen Anbieter'; res.status(400).json({ error: `Eine Tarifoption mit diesem Namen existiert bereits ${scope}` }); } else { res.status(500).json({ error: err.message }); } return; } res.json({ id: this.lastID, message: 'Tarifoption erfolgreich erstellt' }); }); } // Get tarifoptionen for schaltung app.get('/api/schaltungen/:id/tarifoptionen', (req, res) => { const { id } = req.params; const db = getDatabase(); const query = ` SELECT t.*, st.id as schaltung_tarifoption_id FROM tarifoptionen t JOIN schaltungen_tarifoptionen st ON t.id = st.tarifoption_id WHERE st.schaltung_id = ? ORDER BY t.name `; db.all(query, [id], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add tarifoption to schaltung app.post('/api/schaltungen/:id/tarifoptionen', (req, res) => { const schaltung_id = req.params.id; const { tarifoption_id } = req.body; const db = getDatabase(); const query = ` INSERT INTO schaltungen_tarifoptionen (schaltung_id, tarifoption_id) VALUES (?, ?) `; db.run(query, [schaltung_id, tarifoption_id], function(err) { if (err) { if (err.message.includes('UNIQUE constraint failed')) { res.status(400).json({ error: 'Tarifoption bereits hinzugefügt' }); } else { res.status(500).json({ error: err.message }); } return; } res.json({ id: this.lastID, message: 'Tarifoption erfolgreich hinzugefügt' }); }); }); // Remove tarifoption from schaltung app.delete('/api/schaltungen/:schaltung_id/tarifoptionen/:tarifoption_id', (req, res) => { const { schaltung_id, tarifoption_id } = req.params; const db = getDatabase(); db.run('DELETE FROM schaltungen_tarifoptionen WHERE schaltung_id = ? AND tarifoption_id = ?', [schaltung_id, tarifoption_id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Verknüpfung nicht gefunden' }); return; } res.json({ message: 'Tarifoption erfolgreich entfernt' }); }); }); // Create new tarifoptionen with multiple tarif assignments app.post('/api/tarifoptionen/multi', (req, res) => { const { anbieter_id, tarif_ids = [], // Array of tarif IDs is_anbieter_weit = false, name, code, provision_euro, beschreibung } = req.body; if (!anbieter_id || !name) { return res.status(400).json({ error: 'Anbieter und Name sind erforderlich' }); } if (!is_anbieter_weit && (!tarif_ids || tarif_ids.length === 0)) { return res.status(400).json({ error: 'Bei tarif-spezifischen Optionen müssen Tarife ausgewählt werden' }); } const db = getDatabase(); // Insert into new tarifoptionen table const insertQuery = ` INSERT INTO tarifoptionen_new (name, code, provision_euro, beschreibung) VALUES (?, ?, ?, ?) `; db.run(insertQuery, [name, code || null, provision_euro || 0, beschreibung || null], function(err) { if (err) { console.error('Error inserting tarifoption:', err); res.status(500).json({ error: err.message }); return; } const tarifoptionId = this.lastID; if (is_anbieter_weit) { // Get all tarifs for the anbieter db.all('SELECT id FROM tarife WHERE anbieter_id = ?', [anbieter_id], (err, tarifs) => { if (err) { res.status(500).json({ error: err.message }); return; } // Insert into tarifoptionen_tarife for all tarifs const assignments = tarifs.map(tarif => [tarifoptionId, tarif.id]); insertTarifAssignmentsAndLegacy(db, res, tarifoptionId, assignments, name, code, provision_euro, beschreibung, anbieter_id); }); } else { // Insert only selected tarifs const assignments = tarif_ids.map(tarif_id => [tarifoptionId, tarif_id]); insertTarifAssignmentsAndLegacy(db, res, tarifoptionId, assignments, name, code, provision_euro, beschreibung, anbieter_id); } }); }); function insertTarifAssignments(db, res, tarifoptionId, assignments) { const stmt = db.prepare('INSERT INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)'); assignments.forEach(assignment => { stmt.run(assignment, (err) => { if (err) { console.error('Error inserting tarif assignment:', err); } }); }); stmt.finalize((err) => { if (err) { res.status(500).json({ error: err.message }); } else { res.json({ id: tarifoptionId, message: 'Tarifoption erfolgreich erstellt', assignments: assignments.length }); } }); } function insertTarifAssignmentsAndLegacy(db, res, tarifoptionId, assignments, name, code, provision_euro, beschreibung, anbieter_id) { // First insert into new structure (tarifoptionen_tarife) const stmt = db.prepare('INSERT INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)'); assignments.forEach(assignment => { stmt.run(assignment, (err) => { if (err) { console.error('Error inserting tarif assignment:', err); } }); }); stmt.finalize((err) => { if (err) { res.status(500).json({ error: err.message }); return; } // Now also insert into old structure (tarifoptionen) for backward compatibility with schaltungen const legacyStmt = db.prepare('INSERT OR IGNORE INTO tarifoptionen (anbieter_id, tarif_id, name, code, provision_euro, beschreibung) VALUES (?, ?, ?, ?, ?, ?)'); assignments.forEach(assignment => { const tarif_id = assignment[1]; // assignment is [tarifoptionId, tarif_id] legacyStmt.run([anbieter_id, tarif_id, name, code, provision_euro || 0, beschreibung], (err) => { if (err) { console.error('Error inserting legacy tarifoption:', err); } }); }); legacyStmt.finalize((err) => { if (err) { console.error('Error finalizing legacy insert:', err); } res.json({ id: tarifoptionId, message: 'Tarifoption erfolgreich erstellt', assignments: assignments.length }); }); }); } // Get tarifoptionen with their tarif assignments app.get('/api/tarifoptionen/multi', (req, res) => { const db = getDatabase(); const query = ` SELECT tn.id, tn.name, tn.code, tn.provision_euro, tn.beschreibung, tn.aktiv, tn.created_at, GROUP_CONCAT(t.name || ' (' || a.name || ')') as tarif_names, GROUP_CONCAT(t.id) as tarif_ids FROM tarifoptionen_new tn LEFT JOIN tarifoptionen_tarife tt ON tn.id = tt.tarifoption_id LEFT JOIN tarife t ON tt.tarif_id = t.id LEFT JOIN anbieter a ON t.anbieter_id = a.id WHERE tn.aktiv = 1 GROUP BY tn.id, tn.name, tn.code, tn.provision_euro, tn.beschreibung, tn.aktiv, tn.created_at ORDER BY tn.created_at DESC `; db.all(query, [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Delete tarifoption from new structure app.delete('/api/tarifoptionen/multi/:id', (req, res) => { const { id } = req.params; const db = getDatabase(); // First get the tarifoption details to delete from old structure db.get('SELECT name, code FROM tarifoptionen_new WHERE id = ?', [id], (err, option) => { if (err) { res.status(500).json({ error: err.message }); return; } if (!option) { res.status(404).json({ error: 'Tarifoption nicht gefunden' }); return; } // Delete from tarifoptionen_tarife first (foreign key constraint) db.run('DELETE FROM tarifoptionen_tarife WHERE tarifoption_id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } // Delete from old structure (match by name and code - will delete all matching entries across all tarifs) db.run('DELETE FROM tarifoptionen WHERE name = ? AND (code = ? OR (code IS NULL AND ? IS NULL))', [option.name, option.code, option.code], function(err) { if (err) { console.error('Error deleting from old tarifoptionen:', err); // Don't fail the whole operation } console.log(`Deleted ${this.changes} entries from old tarifoptionen table`); }); // Then delete from tarifoptionen_new db.run('DELETE FROM tarifoptionen_new WHERE id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ message: 'Tarifoption erfolgreich gelöscht' }); }); }); }); }); // Update tarifoption in new structure app.put('/api/tarifoptionen/multi/:id', (req, res) => { const { id } = req.params; const { name, code, provision_euro, beschreibung, anbieter_id, is_anbieter_weit, tarif_ids } = req.body; const db = getDatabase(); if (!name) { return res.status(400).json({ error: 'Tarifoption-Name ist erforderlich' }); } db.serialize(() => { db.run('BEGIN TRANSACTION'); // Update the tarifoption itself const updateQuery = ` UPDATE tarifoptionen_new SET name = ?, code = ?, provision_euro = ?, beschreibung = ? WHERE id = ? `; db.run(updateQuery, [name, code, provision_euro || 0, beschreibung, id], function(err) { if (err) { db.run('ROLLBACK'); res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { db.run('ROLLBACK'); res.status(404).json({ error: 'Tarifoption nicht gefunden' }); return; } // Delete existing tarif associations from new structure db.run('DELETE FROM tarifoptionen_tarife WHERE tarifoption_id = ?', [id], function(err) { if (err) { db.run('ROLLBACK'); res.status(500).json({ error: err.message }); return; } // Also delete from old structure (match by name and code for cleanup - will delete all matching entries) db.run('DELETE FROM tarifoptionen WHERE name = ? AND (code = ? OR (code IS NULL AND ? IS NULL))', [name, code, code], function(err) { if (err) { console.error('Error deleting old tarifoptionen:', err); // Don't fail the whole operation if old table cleanup fails } console.log(`Deleted ${this.changes} old tarifoptionen entries for update`); }); // If anbieter-weit, get all tarifs for the anbieter if (is_anbieter_weit && anbieter_id) { db.all('SELECT id FROM tarife WHERE anbieter_id = ?', [anbieter_id], (err, tarifs) => { if (err) { db.run('ROLLBACK'); res.status(500).json({ error: err.message }); return; } // Insert tarif associations for all tarifs of the anbieter let completed = 0; const total = tarifs.length; if (total === 0) { db.run('COMMIT'); res.json({ message: 'Tarifoption erfolgreich aktualisiert' }); return; } tarifs.forEach(tarif => { db.run('INSERT INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)', [id, tarif.id], function(err) { completed++; if (err) { db.run('ROLLBACK'); if (!res.headersSent) { res.status(500).json({ error: err.message }); } return; } // Also insert into old structure db.run('INSERT OR IGNORE INTO tarifoptionen (anbieter_id, tarif_id, name, code, provision_euro, beschreibung) VALUES (?, ?, ?, ?, ?, ?)', [anbieter_id, tarif.id, name, code, provision_euro || 0, beschreibung], function(err) { if (err) { console.error('Error inserting into old tarifoptionen:', err); } }); if (completed === total) { db.run('COMMIT'); if (!res.headersSent) { res.json({ message: 'Tarifoption erfolgreich aktualisiert' }); } } } ); }); }); } else if (tarif_ids && tarif_ids.length > 0) { // Insert specific tarif associations let completed = 0; const total = tarif_ids.length; tarif_ids.forEach(tarifId => { db.run('INSERT INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)', [id, tarifId], function(err) { completed++; if (err) { db.run('ROLLBACK'); if (!res.headersSent) { res.status(500).json({ error: err.message }); } return; } // Also insert into old structure - need to get anbieter_id first db.get('SELECT anbieter_id FROM tarife WHERE id = ?', [tarifId], (err, tarif) => { if (!err && tarif) { db.run('INSERT OR IGNORE INTO tarifoptionen (anbieter_id, tarif_id, name, code, provision_euro, beschreibung) VALUES (?, ?, ?, ?, ?, ?)', [tarif.anbieter_id, tarifId, name, code, provision_euro || 0, beschreibung], function(err) { if (err) { console.error('Error inserting into old tarifoptionen:', err); } }); } }); if (completed === total) { db.run('COMMIT'); if (!res.headersSent) { res.json({ message: 'Tarifoption erfolgreich aktualisiert' }); } } } ); }); } else { db.run('COMMIT'); res.json({ message: 'Tarifoption erfolgreich aktualisiert' }); } }); }); }); }); // Update tarifoption app.put('/api/tarifoptionen/:id', (req, res) => { const { id } = req.params; const { name, provision_betrag } = req.body; const db = getDatabase(); if (!name) { return res.status(400).json({ error: 'Tarifoption-Name ist erforderlich' }); } const query = ` UPDATE tarifoptionen SET name = ?, provision_betrag = ? WHERE id = ? `; db.run(query, [name, provision_betrag || null, id], function(err) { if (err) { if (err.message.includes('UNIQUE constraint failed')) { res.status(400).json({ error: 'Eine Tarifoption mit diesem Namen existiert bereits für diesen Tarif' }); } else { res.status(500).json({ error: err.message }); } return; } if (this.changes === 0) { res.status(404).json({ error: 'Tarifoption nicht gefunden' }); return; } res.json({ message: 'Tarifoption erfolgreich aktualisiert' }); }); }); // Delete tarifoption app.delete('/api/tarifoptionen/:id', (req, res) => { const id = req.params.id; const db = getDatabase(); db.run('DELETE FROM tarifoptionen WHERE id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Tarifoption nicht gefunden' }); return; } res.json({ message: 'Tarifoption erfolgreich gelöscht' }); }); }); // Mitarbeiter-Prozentsätze endpoints // Get mitarbeiter-prozentsaetze for specific anbieter app.get('/api/anbieter/:id/mitarbeiter-prozentsaetze', (req, res) => { const anbieterId = req.params.id; const db = getDatabase(); const query = ` SELECT * FROM mitarbeiter_prozentsaetze WHERE anbieter_id = ? ORDER BY gueltig_ab DESC `; db.all(query, [anbieterId], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // Add new mitarbeiter-prozentsatz app.post('/api/mitarbeiter-prozentsaetze', (req, res) => { const { anbieter_id, prozentsatz, gueltig_ab, beschreibung } = req.body; const db = getDatabase(); const query = ` INSERT INTO mitarbeiter_prozentsaetze ( anbieter_id, prozentsatz, gueltig_ab, beschreibung ) VALUES (?, ?, ?, ?) `; db.run(query, [ anbieter_id, prozentsatz, gueltig_ab || new Date().toISOString().split('T')[0], beschreibung || null ], function(err) { if (err) { if (err.message.includes('UNIQUE constraint failed')) { res.status(400).json({ error: 'Ein Mitarbeiter-Prozentsatz für dieses Datum existiert bereits für diesen Anbieter' }); } else { res.status(500).json({ error: err.message }); } return; } res.json({ id: this.lastID, message: 'Mitarbeiter-Prozentsatz erfolgreich erstellt' }); }); }); // Delete mitarbeiter-prozentsatz app.delete('/api/mitarbeiter-prozentsaetze/:id', (req, res) => { const id = req.params.id; const db = getDatabase(); db.run('DELETE FROM mitarbeiter_prozentsaetze WHERE id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Mitarbeiter-Prozentsatz nicht gefunden' }); return; } res.json({ message: 'Mitarbeiter-Prozentsatz erfolgreich gelöscht' }); }); }); // Update mitarbeiter-provision for tarif app.put('/api/tarife/:id/mitarbeiter-provision', (req, res) => { const tarifId = req.params.id; const { mitarbeiter_provision } = req.body; const db = getDatabase(); db.run('UPDATE tarife SET mitarbeiter_provision = ? WHERE id = ?', [mitarbeiter_provision || 0, tarifId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Tarif nicht gefunden' }); return; } res.json({ message: 'Mitarbeiter-Provision erfolgreich aktualisiert' }); }); }); // Serve main HTML file app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(PORT, () => { console.log(`Server läuft auf http://localhost:${PORT}`); });