Files
prov/server.js
T
kerimatilganandClaude Opus 4.7 55ec916f35 Initial commit: Provisions-Tracker application
Node.js/Express + SQLite + Vanilla JS app for managing employee
commissions on telecom activations (Telekom, Vodafone, O2, 1&1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:07:05 +02:00

2139 lines
73 KiB
JavaScript

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}`);
});