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>
This commit is contained in:
kerimatilganandClaude Opus 4.7 committed 2026-05-14 21:07:05 +02:00
commit 55ec916f35
10 files changed
+11024

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
# Dependencies
node_modules/
# Database (contains real customer data)
provision.db
provision.db.backup
*.db
*.db-journal
# Uploaded files (real customer documents)
uploads/
# Local Claude settings
.claude/
# OS / Editor
.DS_Store
Thumbs.db
*.swp
*.swo
.vscode/
.idea/
# Logs
*.log
npm-debug.log*
+224
View File
@@ -0,0 +1,224 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **Provisions-Tracker** (Commission Tracker) application for managing employee commissions on telecommunications activations ("Schaltungen"). Built as a Node.js/Express backend with SQLite database and vanilla JavaScript frontend.
**Purpose**: Track commissions for telecom providers (Telekom, Vodafone, O2, 1&1), including tariffs, customer data, documents, and complex commission calculations.
## Development Commands
### Running the Application
```bash
# Production mode
npm start
# Development mode with auto-restart
npm run dev
# Install dependencies
npm install
```
The application runs on `http://localhost:3000` by default (configurable via `PORT` environment variable).
### Database
- SQLite database file: `provision.db` (auto-created on first run)
- Database initialization happens automatically via `initDatabase()` in [database.js](database.js)
- Backup file exists: `provision.db.backup`
- No migrations system - uses `CREATE TABLE IF NOT EXISTS` and `ALTER TABLE` with error handling
## Architecture Overview
### Three-Tier Structure
**Backend** ([server.js](server.js) - ~1987 lines)
- Express.js REST API with ~40+ endpoints
- Handles business logic, commission calculations, and database operations
- File upload handling via Multer (10MB limit)
- CORS enabled for development
**Database** ([database.js](database.js) - 288 lines)
- SQLite with 12+ tables
- Key tables: `anbieter` (providers), `tarife` (tariffs), `schaltungen` (connections/commissions), `dokumente` (documents)
- Complex commission system with multiple rule types (fixed/percentage)
- Includes soft-delete tracking (`geloeschte_schaltungen`)
**Frontend** ([public/](public/))
- Single-page app: [index.html](public/index.html), [script.js](public/script.js) (~4000+ lines)
- Tab-based navigation (Erfassung, Übersicht, Provisionen, Admin, Gelöschte)
- Preloading strategy for fast tab switching
- Drag-and-drop file uploads
### Key Database Tables and Relationships
```
anbieter (providers)
├─→ tarife (tariffs) - provision_euro, mitarbeiter_provision
│ ├─→ provisionsregeln (tariff-specific commission rules)
│ └─→ tarifoptionen_tarife (M2M link to options)
├─→ anbieter_provisionsregeln (global provider commission rules)
└─→ mitarbeiter_prozentsaetze (employee percentage rates per provider)
schaltungen (main commissions table)
├─→ anbieter_id, tarif_id (foreign keys)
├─→ dokumente (uploaded documents)
├─→ schaltungen_tarifoptionen (M2M selected tariff options)
└─→ hardware_ek, kundenverkaufspreis_netto (hardware pricing)
tarifoptionen_new (tariff options - new structure)
└─→ tarifoptionen_tarife (M2M relationship to tarifs)
tarifoptionen (old structure - kept for migration compatibility)
```
### Commission Calculation System
The app supports multiple commission calculation methods with complex fallback logic:
1. **Tariff-specific rules** (`provisionsregeln` table)
- Fixed amount or percentage
- Employee commission (verkaeufer_provision)
2. **Global provider rules** (`anbieter_provisionsregeln` table)
- Applied when no tariff-specific rule exists
3. **Direct tariff amounts** (fallback)
- `tarife.provision_euro` - company commission
- `tarife.mitarbeiter_provision` - employee commission
4. **Tariff options** (`tarifoptionen`)
- Additional commission adjustments per activation
- Can be positive or negative (e.g., rebates)
- Many-to-many relationship (options can apply to multiple tariffs)
**API endpoint**: `GET /api/schaltungen/:id/provision` - calculates total commission for a specific activation.
### Migration Notes
The system includes an in-place migration from old tarifoptionen structure (one-to-many with tarifs) to new structure (many-to-many via `tarifoptionen_new` and `tarifoptionen_tarife`). Migration runs automatically on startup via `migrateToNewTarifoptionen()` in [database.js:228-266](database.js#L228-L266).
## API Endpoint Categories
### Anbieter (Providers)
- `GET /api/anbieter` - List all providers
- `POST /api/anbieter` - Add provider
- `DELETE /api/anbieter/:id` - Delete provider (with dependency checks)
- `GET /api/anbieter/:id/provisionsregeln` - Get global commission rules
- `POST /api/anbieter-provisionsregeln` - Add global rule
- `PUT /api/anbieter-provisionsregeln/:id` - Update global rule
### Tarife (Tariffs)
- `GET /api/tarife` - List all tariffs
- `GET /api/tarife/:anbieter_id` - List tariffs for provider
- `GET /api/tarif/:id` - Get single tariff
- `POST /api/tarife` - Add tariff
- `PUT /api/tarife/:id` - Update tariff
- `DELETE /api/tarife/:id` - Delete tariff (with dependency checks)
- `GET /api/tarife/:id/provisionsregeln` - Get tariff commission rules
- `POST /api/provisionsregeln` - Add tariff rule
- `PUT /api/provisionsregeln/:id` - Update tariff rule
- `PUT /api/tarife/:id/mitarbeiter-provision` - Update employee commission
### Schaltungen (Commissions/Activations)
- `GET /api/schaltungen` - List all activations
- `POST /api/schaltungen` - Create activation
- `PUT /api/schaltungen/:id` - Update activation
- `DELETE /api/schaltungen/:id` - Delete activation (soft delete with reason tracking)
- `GET /api/schaltungen/:id/provision` - Calculate commission for activation
- `GET /api/provisionen` - Get commission overview/report
- `GET /api/geloeschte-schaltungen` - List deleted activations
### Dokumente (Documents)
- `POST /api/schaltungen/:id/dokumente` - Upload document (multipart/form-data)
- `GET /api/schaltungen/:id/dokumente` - List documents for activation
- `GET /api/dokumente/:id/download` - Download document
- `DELETE /api/dokumente/:id` - Delete document (removes file from disk)
### Tarifoptionen (Tariff Options)
- `GET /api/tarifoptionen` - List all options (old structure)
- `GET /api/tarifoptionen/multi` - List all options (new structure)
- `POST /api/tarifoptionen/multi` - Add option (new structure)
- `PUT /api/tarifoptionen/multi/:id` - Update option (new structure)
- `DELETE /api/tarifoptionen/multi/:id` - Delete option (new structure)
- `GET /api/schaltungen/:id/tarifoptionen` - Get options for activation
- `POST /api/schaltungen/:id/tarifoptionen` - Link option to activation
- `DELETE /api/schaltungen/:schaltung_id/tarifoptionen/:tarifoption_id` - Unlink option
### Mitarbeiter (Employee Settings)
- `GET /api/anbieter/:id/mitarbeiter-prozentsaetze` - Get employee percentages for provider
- `POST /api/mitarbeiter-prozentsaetze` - Add percentage rate
- `DELETE /api/mitarbeiter-prozentsaetze/:id` - Delete percentage rate
## Important Implementation Details
### File Uploads
- Storage: `uploads/` directory (created automatically)
- Naming: `{timestamp}-{random}.{extension}`
- Limit: 10MB per file
- Supported types: PDF, JPG, JPEG, PNG, DOC, DOCX (enforced client-side)
- Files are physically deleted when document is removed
### Frontend State Management
- **Preloading**: App preloads all tab data in background after 1 second delay for faster navigation
- **Tab switching**: Uses cached data when available (`showTab()` checks if data exists)
- Global variables: `currentSchaltungId`, `uploadedFiles`
- Message system: Toast notifications via `showMessage()` overlay
### Database Connection Pattern
**Singleton Pattern** - A single persistent database connection is used throughout the application lifecycle:
```javascript
const db = getDatabase(); // Returns singleton instance
// ... do work ...
// NO db.close() - connection persists
```
- Connection is opened once on startup and reused
- Foreign keys are enabled after initialization (`PRAGMA foreign_keys = ON`)
- Busy timeout set to 3000ms for concurrent access handling
- Graceful shutdown closes connection on SIGINT/SIGTERM
### Delete Operations
- **Soft delete**: Schaltungen are moved to `geloeschte_schaltungen` table with reason and timestamp
- **CASCADE delete**: Deleting a Schaltung automatically removes:
- Associated documents from database (via `ON DELETE CASCADE`)
- Document files from filesystem (handled in endpoint)
- Linked tarifoptionen (via `ON DELETE CASCADE`)
- **Hard delete**: Individual documents physically removed from filesystem when deleted
- **Dependency checks**: Providers and Tariffs check for usage before deletion
### Language
- **UI/Database**: German (Anbieter, Tarife, Schaltungen, etc.)
- **Code**: Mixed German/English
- **Comments**: Mostly English in code, German in UI strings
## Testing Data
Default data seeded on first run:
- **Providers**: Telekom, Vodafone, O2, 1&1
- **Tariffs**: 10 example tariffs with commission amounts
- **Employee percentages**: 8-10% per provider
- **Tariff options**: 5 example options including rebates
- **Global rules**: Example O2 (fixed 20€) and 1&1 (12% percentage) rules
## Common Development Patterns
### Adding a New API Endpoint
1. Add route in [server.js](server.js) following existing patterns
2. Use `getDatabase()` to open connection
3. Always call `db.close()` in callbacks
4. Return JSON with proper error handling
5. Update frontend in [script.js](script.js) with corresponding API call
### Adding a Database Table
1. Add `CREATE TABLE IF NOT EXISTS` in [database.js](database.js) `initDatabase()`
2. Add within `db.serialize()` block
3. Consider foreign key constraints
4. Add seed data with `INSERT OR IGNORE` if needed
### Modifying Commission Logic
- Main calculation logic is in `GET /api/schaltungen/:id/provision`
- Fallback order: provisionsregeln → anbieter_provisionsregeln → direct tariff amounts
- Always recalculate when tariff/provider/options change
- Employee commission calculated separately from company commission
+66
View File
@@ -0,0 +1,66 @@
# Provisions-Tracker
Ein Tool zur Erfassung und Verwaltung von Mitarbeiter-Provisionen bei Schaltungen.
## Funktionen
- **Schaltungen erfassen**: Vollständige Erfassung aller relevanten Daten
- Schaltungs-ID, Eingabe-Datum, Schaltungs-/Aktivierungsdatum
- Kundendaten (Vorname, Nachname, optional Firma, Rufnummer)
- Anbieter und Tarif (konfigurierbar über Admin-Bereich)
- Hardware (15-stellige Eingabe)
- Dokumentenupload mit Drag & Drop
- **Übersicht**: Tabelle aller erfassten Schaltungen mit Filtermöglichkeiten
- **Administration**:
- Anbieter hinzufügen und verwalten
- Tarife pro Anbieter konfigurieren
## Installation
1. Abhängigkeiten installieren:
```bash
npm install
```
2. Server starten:
```bash
npm start
```
Oder für Entwicklung mit automatischem Neustart:
```bash
npm run dev
```
3. Anwendung im Browser öffnen:
```
http://localhost:3000
```
## Technische Details
- **Backend**: Node.js mit Express
- **Datenbank**: SQLite (wird automatisch erstellt)
- **Frontend**: Vanilla HTML/CSS/JavaScript
- **Datei-Upload**: Multer mit 10MB Limit
## Datenbankstruktur
- `anbieter`: Telekommunikationsanbieter
- `tarife`: Tarife pro Anbieter
- `schaltungen`: Haupttabelle für Provisionserfassung
- `dokumente`: Hochgeladene Dateien pro Schaltung
## Standarddaten
Die Anwendung wird mit folgenden Testdaten initialisiert:
- Anbieter: Telekom, Vodafone, O2, 1&1
- Beispiel-Tarife für jeden Anbieter
## Sicherheitshinweise
- Dateien werden im `uploads/` Verzeichnis gespeichert
- Maximale Dateigröße: 10MB
- Unterstützte Dateitypen: PDF, JPG, JPEG, PNG, DOC, DOCX
+333
View File
@@ -0,0 +1,333 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const dbPath = path.join(__dirname, 'provision.db');
// Singleton database instance
let dbInstance = null;
// Get or create database instance (Singleton pattern)
function getDatabase() {
if (!dbInstance) {
dbInstance = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error opening database:', err.message);
dbInstance = null;
throw err;
}
console.log('Connected to SQLite database');
});
// Set busy timeout to handle concurrent access
dbInstance.configure('busyTimeout', 3000);
}
return dbInstance;
}
// Enable foreign keys for a database connection
function enableForeignKeys(db) {
db.run('PRAGMA foreign_keys = ON', (err) => {
if (err) {
console.error('Error enabling foreign keys:', err.message);
}
});
}
// Close database connection (for graceful shutdown)
function closeDatabase() {
if (dbInstance) {
dbInstance.close((err) => {
if (err) {
console.error('Error closing database:', err.message);
} else {
console.log('Database connection closed');
}
dbInstance = null;
});
}
}
function initDatabase() {
const db = getDatabase();
db.serialize(() => {
// Anbieter (Providers) table
db.run(`CREATE TABLE IF NOT EXISTS anbieter (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Mitarbeiter-Prozentsätze pro Anbieter (can be changed retrospectively)
db.run(`CREATE TABLE IF NOT EXISTS mitarbeiter_prozentsaetze (
id INTEGER PRIMARY KEY AUTOINCREMENT,
anbieter_id INTEGER NOT NULL,
prozentsatz DECIMAL(5,2) NOT NULL DEFAULT 0,
gueltig_ab DATE DEFAULT CURRENT_DATE,
beschreibung TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (anbieter_id) REFERENCES anbieter (id),
UNIQUE(anbieter_id, gueltig_ab)
)`);
// Tarife (Tariffs) table - simplified with fixed euro amounts only
db.run(`CREATE TABLE IF NOT EXISTS tarife (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
anbieter_id INTEGER,
provision_euro DECIMAL(10,2) DEFAULT 0,
mitarbeiter_provision DECIMAL(10,2) DEFAULT 0,
beschreibung TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (anbieter_id) REFERENCES anbieter (id),
UNIQUE(name, anbieter_id)
)`);
// Add mitarbeiter_provision column if it doesn't exist (for existing databases)
db.run(`ALTER TABLE tarife ADD COLUMN mitarbeiter_provision DECIMAL(10,2) DEFAULT 0`, (err) => {
// Ignore error if column already exists
});
// Provision rules table (tariff-specific)
db.run(`CREATE TABLE IF NOT EXISTS provisionsregeln (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tarif_id INTEGER NOT NULL,
provision_typ TEXT NOT NULL CHECK(provision_typ IN ('fix', 'prozent')),
betrag DECIMAL(10,2) NOT NULL,
beschreibung TEXT,
verkaeufer_provision_typ TEXT CHECK(verkaeufer_provision_typ IN ('fix', 'prozent')),
verkaeufer_provision_betrag DECIMAL(10,2),
verkaeufer_provision_beschreibung TEXT,
gueltig_ab DATE DEFAULT CURRENT_DATE,
aktiv BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tarif_id) REFERENCES tarife (id)
)`);
// Global provider commission rules table
db.run(`CREATE TABLE IF NOT EXISTS anbieter_provisionsregeln (
id INTEGER PRIMARY KEY AUTOINCREMENT,
anbieter_id INTEGER NOT NULL,
provision_typ TEXT NOT NULL CHECK(provision_typ IN ('fix', 'prozent')),
betrag DECIMAL(10,2) NOT NULL,
beschreibung TEXT,
verkaeufer_provision_typ TEXT CHECK(verkaeufer_provision_typ IN ('fix', 'prozent')),
verkaeufer_provision_betrag DECIMAL(10,2),
verkaeufer_provision_beschreibung TEXT,
gueltig_ab DATE DEFAULT CURRENT_DATE,
aktiv BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (anbieter_id) REFERENCES anbieter (id)
)`);
// Schaltungen (Connections/Commissions) table
db.run(`CREATE TABLE IF NOT EXISTS schaltungen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
schaltungs_id TEXT NOT NULL UNIQUE,
eingabe_datum DATE NOT NULL,
schaltungs_datum DATE NOT NULL,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
firma TEXT,
rufnummer TEXT NOT NULL,
anbieter_id INTEGER NOT NULL,
tarif_id INTEGER NOT NULL,
hardware TEXT,
hardware_ek DECIMAL(10,2),
kundenverkaufspreis_netto DECIMAL(10,2),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (anbieter_id) REFERENCES anbieter (id),
FOREIGN KEY (tarif_id) REFERENCES tarife (id)
)`);
// Add new hardware pricing columns if they don't exist (for existing databases)
db.run(`ALTER TABLE schaltungen ADD COLUMN hardware_ek DECIMAL(10,2)`, (err) => {
// Ignore error if column already exists
});
db.run(`ALTER TABLE schaltungen ADD COLUMN kundenverkaufspreis_netto DECIMAL(10,2)`, (err) => {
// Ignore error if column already exists
});
// Dokumente (Documents) table
db.run(`CREATE TABLE IF NOT EXISTS dokumente (
id INTEGER PRIMARY KEY AUTOINCREMENT,
schaltung_id INTEGER NOT NULL,
filename TEXT NOT NULL,
original_filename TEXT NOT NULL,
filepath TEXT NOT NULL,
filesize INTEGER,
mimetype TEXT,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (schaltung_id) REFERENCES schaltungen (id) ON DELETE CASCADE
)`);
// Deleted Schaltungen tracking table
db.run(`CREATE TABLE IF NOT EXISTS geloeschte_schaltungen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_id INTEGER NOT NULL,
schaltungs_id TEXT NOT NULL,
eingabe_datum DATE NOT NULL,
schaltungs_datum DATE NOT NULL,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
firma TEXT,
rufnummer TEXT NOT NULL,
anbieter_id INTEGER NOT NULL,
tarif_id INTEGER NOT NULL,
hardware TEXT,
anbieter_name TEXT NOT NULL,
tarif_name TEXT NOT NULL,
loeschgrund TEXT NOT NULL,
geloescht_am DATETIME DEFAULT CURRENT_TIMESTAMP,
geloescht_von TEXT DEFAULT 'System'
)`);
// Insert some default providers and tariffs for testing
db.run(`INSERT OR IGNORE INTO anbieter (name) VALUES
('Telekom'),
('Vodafone'),
('O2'),
('1&1')`);
db.run(`INSERT OR IGNORE INTO tarife (name, anbieter_id, provision_euro, beschreibung) VALUES
('MagentaMobil S', 1, 150.00, 'Telekom Basis-Tarif'),
('MagentaMobil M', 1, 200.00, 'Telekom Standard-Tarif'),
('MagentaMobil L', 1, 250.00, 'Telekom Premium-Tarif'),
('VF green LTE 10GB mit Smartphone 5', 2, 190.00, 'Vodafone Standard-Paket'),
('Red S', 2, 120.00, 'Vodafone Einsteiger-Tarif'),
('Red M', 2, 160.00, 'Vodafone Standard-Tarif'),
('Red L', 2, 200.00, 'Vodafone Premium-Tarif'),
('o2 Free S', 3, 100.00, 'O2 Basis-Tarif'),
('o2 Free M', 3, 140.00, 'O2 Standard-Tarif'),
('o2 Free L', 3, 180.00, 'O2 Premium-Tarif')`);
// Insert mitarbeiter prozentsätze for each provider
db.run(`INSERT OR IGNORE INTO mitarbeiter_prozentsaetze (anbieter_id, prozentsatz, beschreibung) VALUES
(1, 8.00, 'Telekom Mitarbeiter-Provision'),
(2, 10.00, 'Vodafone/Freenet Mitarbeiter-Provision'),
(3, 7.50, 'O2 Mitarbeiter-Provision'),
(4, 9.00, '1&1 Mitarbeiter-Provision')`);
// Insert some example global provider commission rules
db.run(`INSERT OR IGNORE INTO anbieter_provisionsregeln (anbieter_id, provision_typ, betrag, beschreibung) VALUES
(3, 'fix', 20.00, 'Standard O2 Provision'),
(4, 'prozent', 12.00, '12% Standard 1&1 Provision')`);
// Tarifoptionen - updated structure for multiple tarif support
db.run(`CREATE TABLE IF NOT EXISTS tarifoptionen_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT,
provision_euro DECIMAL(10,2) DEFAULT 0,
beschreibung TEXT,
aktiv BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Many-to-many relationship between tarifoptionen and tarife
db.run(`CREATE TABLE IF NOT EXISTS tarifoptionen_tarife (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tarifoption_id INTEGER NOT NULL,
tarif_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tarifoption_id) REFERENCES tarifoptionen_new (id) ON DELETE CASCADE,
FOREIGN KEY (tarif_id) REFERENCES tarife (id) ON DELETE CASCADE,
UNIQUE(tarifoption_id, tarif_id)
)`);
// Keep old table for migration compatibility
db.run(`CREATE TABLE IF NOT EXISTS tarifoptionen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tarif_id INTEGER NOT NULL,
name TEXT NOT NULL,
code TEXT,
provision_euro DECIMAL(10,2) DEFAULT 0,
beschreibung TEXT,
aktiv BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tarif_id) REFERENCES tarife (id),
UNIQUE(tarif_id, name)
)`);
// Schaltungen_Tarifoptionen (Many-to-Many relationship)
db.run(`CREATE TABLE IF NOT EXISTS schaltungen_tarifoptionen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
schaltung_id INTEGER NOT NULL,
tarifoption_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (schaltung_id) REFERENCES schaltungen (id) ON DELETE CASCADE,
FOREIGN KEY (tarifoption_id) REFERENCES tarifoptionen (id),
UNIQUE(schaltung_id, tarifoption_id)
)`);
// Insert example tarifoptionen with simplified euro amounts (old format)
db.run(`INSERT OR IGNORE INTO tarifoptionen (tarif_id, name, code, provision_euro, beschreibung) VALUES
(1, 'Magenta Premium Option', 'MAG-PREM', 50.00, 'Premium-Zusatz für Telekom'),
(1, 'Aktionscode Neukunde', 'NEU2024', 30.00, 'Neukunden-Bonus'),
(4, '24 x 17€ MGP Rabatt GK A3958457', 'A3958457', -120.00, 'Vodafone Rabatt-Aktion'),
(5, 'Red M Aktion Q1', 'RED-M-Q1', 25.00, 'Quartals-Aktion'),
(6, 'Red L Premium Paket', 'RED-L-PREM', 35.00, 'Premium-Paket')`);
// Migration function to move data from old to new structure
function migrateToNewTarifoptionen() {
db.serialize(() => {
// Check if migration is needed
db.get(`SELECT COUNT(*) as count FROM tarifoptionen_new`, (err, row) => {
if (err || row.count > 0) return; // Already migrated or error
console.log('Migrating tarifoptionen to new structure...');
// Get all unique tarifoptionen from old table
db.all(`SELECT DISTINCT name, code, provision_euro, beschreibung, aktiv, created_at FROM tarifoptionen`, (err, oldOptions) => {
if (err || !oldOptions.length) return;
oldOptions.forEach(option => {
// Insert unique tarifoption into new table
db.run(`INSERT OR IGNORE INTO tarifoptionen_new (name, code, provision_euro, beschreibung, aktiv, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[option.name, option.code, option.provision_euro, option.beschreibung, option.aktiv, option.created_at],
function(err) {
if (err) return;
const newTarifoptionId = this.lastID;
// Link this tarifoption to all tarifs it was originally assigned to
db.all(`SELECT tarif_id FROM tarifoptionen WHERE name = ? AND code = ?`,
[option.name, option.code],
(err, tarifs) => {
if (err) return;
tarifs.forEach(tarif => {
db.run(`INSERT OR IGNORE INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)`,
[newTarifoptionId, tarif.tarif_id]);
});
});
});
});
});
});
});
}
// Run migration only if not already done
db.get(`SELECT COUNT(*) as count FROM tarifoptionen_new`, (err, row) => {
if (!err && row && row.count === 0) {
migrateToNewTarifoptionen();
}
});
// Keep existing individual tariff rules for backward compatibility
db.run(`INSERT OR IGNORE INTO provisionsregeln (tarif_id, provision_typ, betrag, beschreibung) VALUES
(1, 'fix', 25.00, 'Feste Provision für MagentaMobil S'),
(2, 'fix', 35.00, 'Feste Provision für MagentaMobil M'),
(3, 'fix', 45.00, 'Feste Provision für MagentaMobil L'),
(4, 'prozent', 15.00, '15% Provision für Red S'),
(5, 'prozent', 18.00, '18% Provision für Red M'),
(6, 'prozent', 20.00, '20% Provision für Red L')`);
});
// Enable foreign keys AFTER all tables and initial data are created
enableForeignKeys(db);
}
module.exports = { initDatabase, getDatabase, closeDatabase };
+2608
View File
File diff suppressed because it is too large. Load diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "provision-tracker",
"version": "1.0.0",
"description": "Tool for tracking employee commissions",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"multer": "^1.4.5-lts.1",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
+184
View File
@@ -0,0 +1,184 @@
/**
* Commission Calculation Service
*
* This service handles all commission calculations for schaltungen.
* It implements a fallback hierarchy for determining commission rates:
* 1. Direct tariff commission (highest priority)
* 2. Tariff-specific rules
* 3. Global provider rules (fallback)
* 4. Tariff options (additive)
*/
/**
* Calculate shop commission based on hierarchy
* @param {Object} schaltungData - Data from database including tariff and provider info
* @param {Array} tarifoptionen - Selected tariff options for this schaltung
* @returns {Object} Calculation result with amount, type, description, and source
*/
function calculateShopProvision(schaltungData, tarifoptionen) {
let provision_typ = null;
let provision_betrag = null;
let provision_beschreibung = null;
let berechnete_provision = 0;
let quelle = 'Keine Shop-Provision definiert';
// Priority 1: Direct tariff commission
if (schaltungData.tarif_provision_typ && schaltungData.tarif_provision_betrag) {
provision_typ = schaltungData.tarif_provision_typ;
provision_betrag = schaltungData.tarif_provision_betrag;
provision_beschreibung = schaltungData.tarif_provision_beschreibung;
quelle = 'Direkte Tarif-Provision';
}
// Priority 2: Tariff-specific rule
else if (schaltungData.regel_provision_typ && schaltungData.regel_provision_betrag) {
provision_typ = schaltungData.regel_provision_typ;
provision_betrag = schaltungData.regel_provision_betrag;
provision_beschreibung = schaltungData.regel_provision_beschreibung;
quelle = 'Tarif-spezifische Regel';
}
// Priority 3: Global provider rule as fallback
else if (schaltungData.global_provision_typ && schaltungData.global_provision_betrag) {
provision_typ = schaltungData.global_provision_typ;
provision_betrag = schaltungData.global_provision_betrag;
provision_beschreibung = schaltungData.global_provision_beschreibung;
quelle = 'Globale Anbieter-Regel';
}
// Calculate base commission
if (provision_typ && provision_betrag) {
if (provision_typ === 'fix') {
berechnete_provision = parseFloat(provision_betrag);
} else if (provision_typ === 'prozent') {
const basisBetrag = 100; // This could come from tariff table
berechnete_provision = (parseFloat(provision_betrag) / 100) * basisBetrag;
}
}
// Add tarifoptionen to 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);
}
});
}
berechnete_provision += tarifoptionen_provision;
// Update description if tarifoptionen were added
if (tarifoptionen_provision > 0) {
const tarifoptionen_text = verwendete_tarifoptionen.join(', ');
provision_beschreibung = provision_beschreibung
? `${provision_beschreibung} + Tarifoptionen: ${tarifoptionen_text}`
: `Tarifoptionen: ${tarifoptionen_text}`;
}
return {
typ: provision_typ,
betrag: provision_betrag,
beschreibung: provision_beschreibung,
berechnete_provision: berechnete_provision,
quelle: quelle,
tarifoptionen_provision: tarifoptionen_provision
};
}
/**
* Calculate employee commission based on hierarchy
* @param {Object} schaltungData - Data from database including tariff and provider info
* @param {number} shopProvision - Calculated shop commission (for percentage calculations)
* @returns {Object} Calculation result with amount, type, description, and source
*/
function calculateMitarbeiterProvision(schaltungData, shopProvision) {
let provision_typ = null;
let provision_betrag = null;
let provision_beschreibung = null;
let berechnete_provision = 0;
let quelle = 'Keine Mitarbeiter-Provision definiert';
// Priority 1: Direct tariff employee commission
if (schaltungData.tarif_verkaeufer_provision_typ && schaltungData.tarif_verkaeufer_provision_betrag) {
provision_typ = schaltungData.tarif_verkaeufer_provision_typ;
provision_betrag = schaltungData.tarif_verkaeufer_provision_betrag;
provision_beschreibung = schaltungData.tarif_verkaeufer_provision_beschreibung;
quelle = 'Direkte Tarif-Verkäufer-Provision';
}
// Priority 2: Tariff-specific rule employee commission
else if (schaltungData.regel_verkaeufer_provision_typ && schaltungData.regel_verkaeufer_provision_betrag) {
provision_typ = schaltungData.regel_verkaeufer_provision_typ;
provision_betrag = schaltungData.regel_verkaeufer_provision_betrag;
provision_beschreibung = schaltungData.regel_verkaeufer_provision_beschreibung;
quelle = 'Tarif-spezifische Verkäufer-Regel';
}
// Priority 3: Global provider rule employee commission
else if (schaltungData.global_verkaeufer_provision_typ && schaltungData.global_verkaeufer_provision_betrag) {
provision_typ = schaltungData.global_verkaeufer_provision_typ;
provision_betrag = schaltungData.global_verkaeufer_provision_betrag;
provision_beschreibung = schaltungData.global_verkaeufer_provision_beschreibung;
quelle = 'Globale Anbieter-Verkäufer-Regel';
}
// Calculate employee commission
if (provision_typ && provision_betrag) {
if (provision_typ === 'fix') {
berechnete_provision = parseFloat(provision_betrag);
} else if (provision_typ === 'prozent') {
// If percentage, calculate based on shop commission
berechnete_provision = (parseFloat(provision_betrag) / 100) * shopProvision;
}
}
return {
typ: provision_typ,
betrag: provision_betrag,
beschreibung: provision_beschreibung,
berechnete_provision: berechnete_provision,
quelle: quelle
};
}
/**
* Main function to calculate all commissions for a schaltung
* @param {Object} schaltungData - Complete schaltung data with tariff and provider info
* @param {Array} tarifoptionen - Selected tariff options
* @returns {Object} Complete commission calculation result
*/
function calculateProvision(schaltungData, tarifoptionen) {
const shopResult = calculateShopProvision(schaltungData, tarifoptionen);
const mitarbeiterResult = calculateMitarbeiterProvision(schaltungData, shopResult.berechnete_provision);
return {
schaltung: {
id: schaltungData.id,
schaltungs_id: schaltungData.schaltungs_id,
tarif: schaltungData.tarif_name,
anbieter: schaltungData.anbieter_name
},
shop_provision: {
typ: shopResult.typ,
betrag: shopResult.betrag,
beschreibung: shopResult.beschreibung,
berechnete_provision: shopResult.berechnete_provision,
quelle: shopResult.quelle
},
mitarbeiter_provision: {
typ: mitarbeiterResult.typ,
betrag: mitarbeiterResult.betrag,
beschreibung: mitarbeiterResult.beschreibung,
berechnete_provision: mitarbeiterResult.berechnete_provision,
quelle: mitarbeiterResult.quelle,
prozentsatz: schaltungData.mitarbeiter_prozentsatz || 0
},
tarifoptionen: tarifoptionen || [],
tarifoptionen_provision: shopResult.tarifoptionen_provision
};
}
module.exports = {
calculateProvision,
calculateShopProvision,
calculateMitarbeiterProvision
};
+1799
View File
File diff suppressed because it is too large. Load diff
+3626
View File
File diff suppressed because it is too large. Load diff
+2139
View File
File diff suppressed because it is too large. Load diff