commit 55ec916f357208f83054c98060b711ff7231d4f0 Author: Kerim Atilgan Date: Thu May 14 21:07:05 2026 +0200 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10ee19c --- /dev/null +++ b/.gitignore @@ -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* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b355de8 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b916536 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/database.js b/database.js new file mode 100644 index 0000000..9dfb99f --- /dev/null +++ b/database.js @@ -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 }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0f093e1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2608 @@ +{ + "name": "provision-tracker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "provision-tracker", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "multer": "^1.4.5-lts.1", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..04010d0 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/provisionService.js b/provisionService.js new file mode 100644 index 0000000..a514440 --- /dev/null +++ b/provisionService.js @@ -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 +}; diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..0a48a10 --- /dev/null +++ b/public/index.html @@ -0,0 +1,1799 @@ + + + + + + Provisions-Tracker + + + +
+
+

Provisions-Tracker

+

Erfassung und Verwaltung von Mitarbeiter-Provisionen

+
+ + + + +
+

Neue Schaltung erfassen

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + + + +
+ + +
+ + + + +
+ +
+

Dateien hier hineinziehen oder klicken zum Auswählen

+ +
+
+
+ + +
+
+ + +
+

Schaltungs-Übersicht

+ + +
+

Filter

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + +
Schaltungs-IDEingabe-DatumSchaltungs-DatumNameFirmaRufnummerAnbieterTarifHardwareDokumenteAktionen
+
+
+ + +
+

Provisions-Übersicht

+ + +
+

Filter

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+

Shop-Provision: 0,00 €

+

Mitarbeiter-Provision: 0,00 €

+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
Schaltungs-IDDatumKundeAnbieterTarifShop-ProvisionMitarbeiter-Provision
BetragBetrag
+
+
+
+ + +
+

Gelöschte Schaltungen

+
+ + + + + + + + + + + + + + + + + +
Schaltungs-IDEingabe-DatumSchaltungs-DatumNameFirmaRufnummerAnbieterTarifHardwareLöschgrundGelöscht am
+
+
+ + +
+ +
+

Administration

+
+ + + + + + + +
+
+ + +
+
+
+

Anbieter verwalten

+

Hier können Sie Telekommunikationsanbieter hinzufügen und verwalten.

+
+ +
+

Neuen Anbieter hinzufügen

+
+ + +
+
+ +
+

Bestehende Anbieter

+
+ + + + + + + + + + + + +
IDAnbieter-NameErstellt amAktionen
+
+
+
+
+ + +
+
+
+

Tarife verwalten

+

Verwalten Sie Tarife und deren Provisionen für jeden Anbieter.

+
+ +
+

Neuen Tarif hinzufügen

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Bestehende Tarife

+
+ + + + + + + + + + + + + + +
IDAnbieterTarif-NameProvision (€)BeschreibungAktionen
+
+
+
+
+ + +
+
+
+

Tarifoptionen verwalten

+

Verwalten Sie Zusatzoptionen und Aktionscodes für Tarife.

+
+ +
+
+
+

Tarifoptionen verwalten

+

Erstellen und verwalten Sie Zusatzoptionen für Tarife

+
+ +
+
+ +
+
+

Bestehende Tarifoptionen

+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + +
IDAnbieterTarifOptionCodeProvision (€)BeschreibungAktionen
+
+
+
+
+ + +
+
+
+

Mitarbeiter-Provisionen

+

Verwalten Sie die Prozentsätze für Mitarbeiter-Provisionen pro Anbieter.

+
+ +
+

Mitarbeiter-Prozentsatz festlegen

+
+
+ + + % +
+
+ + +
+ +
+
+ +
+

Aktuelle Mitarbeiter-Prozentsätze

+
+
+
+
+ + +
+
+
+

Mitarbeiter-Provisionen pro Tarif

+

Legen Sie für jeden Tarif individuell die Mitarbeiter-Provision fest.

+
+ +
+

Tarife und Mitarbeiter-Provisionen

+
+
+
+
+ + +
+
+
+

Provisionsregeln verwalten

+

Verwalten Sie spezifische Provisionsregeln für Tarife.

+
+ +
+

Neue Provisionsregel hinzufügen

+
+ + + +
+
+ +
+

Bestehende Provisionsregeln

+
+
+
+
+ + +
+
+
+

Globale Provisionsregeln verwalten

+

Verwalten Sie globale Provisionsregeln für Anbieter.

+
+ +
+

Neue globale Provisionsregel hinzufügen

+
+ + + +
+
+ +
+

Bestehende globale Provisionsregeln

+
+
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + +
+
+
+

📄 Dokumente

+ +
+
+ +
+ +
+
+ + +
+
+
+

📋 PDF Viewer

+ +
+
+
+ +
+
+
+ +
+ Seite 1 von 1 +
+ +
+ + 100% + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..a047f05 --- /dev/null +++ b/public/script.js @@ -0,0 +1,3626 @@ +// Global variables +let currentSchaltungId = null; +let uploadedFiles = []; + +// Initialize the application with background loading +document.addEventListener('DOMContentLoaded', function() { + loadAnbieter(); + loadSchaltungen(); + setupFileUpload(); + + // Set today's date as default + const today = new Date().toISOString().split('T')[0]; + document.getElementById('eingabeDatum').value = today; + + // Setup form submission + document.getElementById('schaltungForm').addEventListener('submit', handleSchaltungSubmit); + + // Load all data in background for faster tab switching + preloadAllData(); +}); + +// Preload all data in background +async function preloadAllData() { + console.log('Preloading all data in background...'); + + try { + // Load data for all tabs simultaneously (with delay to not block UI) + setTimeout(async () => { + const loadPromises = [ + loadAdminData(), // Admin data + loadProvisionen(), // Provisionen tab + loadGeloeschteSchaltungen() // Gelöschte Schaltungen + ]; + + // Execute all loads in parallel + await Promise.allSettled(loadPromises); + + console.log('Background data loading completed'); + + // Show completion message briefly + showMessage('Hintergrunddaten erfolgreich geladen - Tab-Wechsel sind jetzt schneller! ⚡'); + + // Hide loading indicator if it exists + const loadingIndicator = document.getElementById('loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.style.display = 'none'; + } + }, 1000); // Load after 1 second to not interfere with initial load + + } catch (error) { + console.error('Error during background loading:', error); + } +} + +// Tab switching +function showTab(tabName) { + // Hide all tabs + const tabs = document.querySelectorAll('.tab-content'); + tabs.forEach(tab => tab.classList.remove('active')); + + const navTabs = document.querySelectorAll('.nav-tab'); + navTabs.forEach(tab => tab.classList.remove('active')); + + // Show selected tab + document.getElementById(tabName).classList.add('active'); + if (event && event.target) { + event.target.classList.add('active'); + } + + // Optimized loading - only reload if necessary (data should be preloaded) + if (tabName === 'overview') { + // Always refresh main data as it changes most frequently + loadSchaltungen(); + } else if (tabName === 'provisionen') { + // Check if data is already loaded, if not load it + const provisionenTable = document.querySelector('#provisionenTable tbody'); + if (!provisionenTable || !provisionenTable.innerHTML.trim()) { + loadProvisionen(); + } + } else if (tabName === 'geloeschte') { + // Check if data is already loaded, if not load it + if (!document.getElementById('geloeschteList') || !document.getElementById('geloeschteList').innerHTML.trim()) { + loadGeloeschteSchaltungen(); + } + } else if (tabName === 'admin') { + // Check if admin data is already loaded + if (!document.getElementById('anbieterList') || !document.getElementById('anbieterList').innerHTML.trim()) { + loadAdminData(); + } + } +} + +// API Helper functions +async function apiRequest(url, options = {}) { + try { + const response = await fetch(url, options); + + // Check if response has content + const contentType = response.headers.get('content-type'); + let data = null; + + if (contentType && contentType.includes('application/json')) { + const text = await response.text(); + if (text.trim()) { + data = JSON.parse(text); + } + } + + if (!response.ok) { + throw new Error(data?.error || `HTTP ${response.status}: ${response.statusText}`); + } + + return data; + } catch (error) { + showMessage('Fehler: ' + error.message, 'error'); + throw error; + } +} + +// Message display as overlay +function showMessage(message, type = 'success') { + // Create overlay container if it doesn't exist + let overlay = document.getElementById('message-overlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'message-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + pointer-events: none; + max-width: 400px; + `; + document.body.appendChild(overlay); + } + + // Create message element + const messageDiv = document.createElement('div'); + messageDiv.className = `message-toast ${type}`; + messageDiv.textContent = message; + messageDiv.style.cssText = ` + background: ${type === 'success' ? '#d4edda' : '#f8d7da'}; + border: 1px solid ${type === 'success' ? '#c3e6cb' : '#f5c6cb'}; + color: ${type === 'success' ? '#155724' : '#721c24'}; + padding: 12px 16px; + margin-bottom: 10px; + border-radius: 6px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + pointer-events: auto; + animation: slideInRight 0.3s ease-out; + font-family: Arial, sans-serif; + font-size: 14px; + line-height: 1.4; + cursor: pointer; + transition: opacity 0.3s ease-out; + `; + + // Add CSS animation if not already added + if (!document.getElementById('message-animations')) { + const style = document.createElement('style'); + style.id = 'message-animations'; + style.textContent = ` + @keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + @keyframes slideOutRight { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } + `; + document.head.appendChild(style); + } + + // Click to dismiss + messageDiv.addEventListener('click', () => { + messageDiv.style.animation = 'slideOutRight 0.3s ease-out'; + setTimeout(() => messageDiv.remove(), 300); + }); + + overlay.appendChild(messageDiv); + + // Auto-remove after 3 seconds + setTimeout(() => { + if (messageDiv.parentNode) { + messageDiv.style.animation = 'slideOutRight 0.3s ease-out'; + setTimeout(() => messageDiv.remove(), 300); + } + }, 3000); +} + +// Load Anbieter +async function loadAnbieter() { + try { + const anbieter = await apiRequest('/api/anbieter'); + + const anbieterSelect = document.getElementById('anbieter'); + const tarifAnbieterSelect = document.getElementById('tarifAnbieter'); + + // Clear existing options + anbieterSelect.innerHTML = ''; + tarifAnbieterSelect.innerHTML = ''; + + anbieter.forEach(a => { + anbieterSelect.innerHTML += ``; + tarifAnbieterSelect.innerHTML += ``; + }); + + } catch (error) { + console.error('Fehler beim Laden der Anbieter:', error); + } +} + +// Load Tarife for selected Anbieter +async function loadTarife() { + const anbieterId = document.getElementById('anbieter').value; + const tarifSelect = document.getElementById('tarif'); + + // Hide tarifoptionen section when anbieter changes + hideTarifoptionen(); + + if (!anbieterId) { + tarifSelect.innerHTML = ''; + return; + } + + try { + const tarife = await apiRequest(`/api/tarife/${anbieterId}`); + + tarifSelect.innerHTML = ''; + tarife.forEach(t => { + tarifSelect.innerHTML += ``; + }); + + } catch (error) { + console.error('Fehler beim Laden der Tarife:', error); + tarifSelect.innerHTML = ''; + } +} + +// Load tarifoptionen when tarif is selected +async function loadTarifoptionen() { + const tarifSelect = document.getElementById('tarif'); + const tarifId = tarifSelect.value; + + if (!tarifId) { + hideTarifoptionen(); + return; + } + + try { + const tarifoptionen = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`); + displayTarifoptionen(tarifoptionen); + } catch (error) { + console.error('Fehler beim Laden der Tarifoptionen:', error); + hideTarifoptionen(); + } +} + +function displayTarifoptionen(tarifoptionen) { + const section = document.getElementById('tarifoptionenSection'); + const list = document.getElementById('tarifoptionenList'); + const noOptions = document.getElementById('noTarifoptionen'); + + // Clear existing options + list.innerHTML = ''; + + if (tarifoptionen.length === 0) { + section.style.display = 'block'; + list.style.display = 'none'; + noOptions.style.display = 'block'; + } else { + section.style.display = 'block'; + list.style.display = 'grid'; + noOptions.style.display = 'none'; + + tarifoptionen.forEach(option => { + const optionDiv = document.createElement('div'); + optionDiv.style.cssText = 'padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #f9f9f9;'; + + optionDiv.innerHTML = ` + + `; + + list.appendChild(optionDiv); + }); + } +} + +function hideTarifoptionen() { + const section = document.getElementById('tarifoptionenSection'); + section.style.display = 'none'; +} + +// Handle Schaltung form submission +async function handleSchaltungSubmit(e) { + e.preventDefault(); + + const formData = new FormData(e.target); + const schaltungData = Object.fromEntries(formData.entries()); + + // Collect selected tarifoptionen + const selectedTarifoptionen = Array.from(document.querySelectorAll('input[name="tarifoptionen"]:checked')) + .map(checkbox => parseInt(checkbox.value)); + + // Validate hardware length if provided + if (schaltungData.hardware && schaltungData.hardware.length !== 15) { + showMessage('Hardware muss genau 15 Zeichen haben (oder leer lassen)', 'error'); + return; + } + + try { + const response = await apiRequest('/api/schaltungen', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(schaltungData) + }); + + currentSchaltungId = response.id; + + // Add selected tarifoptionen to the schaltung + if (selectedTarifoptionen.length > 0) { + await addTarifoptionenToSchaltung(currentSchaltungId, selectedTarifoptionen); + } + + // Upload files if any + if (uploadedFiles.length > 0) { + await uploadDocuments(currentSchaltungId); + } + + showMessage('Schaltung erfolgreich gespeichert!'); + + // Reset form + e.target.reset(); + uploadedFiles = []; + document.getElementById('uploadedFiles').innerHTML = ''; + hideTarifoptionen(); // Hide tarifoptionen section + + const today = new Date().toISOString().split('T')[0]; + document.getElementById('eingabeDatum').value = today; + + // Reload overview and provisions if needed + await refreshDataAfterSchaltungChange(); + + } catch (error) { + console.error('Fehler beim Speichern der Schaltung:', error); + } +} + +// Add tarifoptionen to schaltung +async function addTarifoptionenToSchaltung(schaltungId, tarifoptionenIds) { + for (const tarifoptionId of tarifoptionenIds) { + try { + await apiRequest(`/api/schaltungen/${schaltungId}/tarifoptionen`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ tarifoption_id: tarifoptionId }) + }); + } catch (error) { + console.warn(`Fehler beim Hinzufügen der Tarifoption ${tarifoptionId}:`, error); + } + } +} + +// File upload setup +function setupFileUpload() { + const uploadArea = document.getElementById('fileUploadArea'); + const fileInput = document.getElementById('documentUpload'); + + uploadArea.addEventListener('click', () => fileInput.click()); + + uploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + uploadArea.classList.add('dragover'); + }); + + uploadArea.addEventListener('dragleave', () => { + uploadArea.classList.remove('dragover'); + }); + + uploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + handleFiles(e.dataTransfer.files); + }); + + fileInput.addEventListener('change', (e) => { + handleFiles(e.target.files); + }); +} + +function handleFiles(files) { + Array.from(files).forEach(file => { + if (file.size > 10 * 1024 * 1024) { + showMessage(`Datei ${file.name} ist zu groß (max. 10MB)`, 'error'); + return; + } + + uploadedFiles.push(file); + displayUploadedFile(file); + }); +} + +function displayUploadedFile(file) { + const filesDiv = document.getElementById('uploadedFiles'); + const fileDiv = document.createElement('div'); + fileDiv.className = 'document-item'; + + fileDiv.innerHTML = ` + ${file.name} (${formatFileSize(file.size)}) + + `; + + filesDiv.appendChild(fileDiv); +} + +function removeFile(fileName) { + uploadedFiles = uploadedFiles.filter(file => file.name !== fileName); + + const filesDiv = document.getElementById('uploadedFiles'); + const fileItems = filesDiv.querySelectorAll('.document-item'); + fileItems.forEach(item => { + if (item.querySelector('span').textContent.startsWith(fileName)) { + item.remove(); + } + }); +} + +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +async function uploadDocuments(schaltungId) { + for (const file of uploadedFiles) { + const formData = new FormData(); + formData.append('document', file); + + try { + await fetch(`/api/schaltungen/${schaltungId}/dokumente`, { + method: 'POST', + body: formData + }); + } catch (error) { + showMessage(`Fehler beim Hochladen von ${file.name}`, 'error'); + } + } +} + +// Refresh all related data when schaltungen change +async function refreshDataAfterSchaltungChange() { + // Always refresh schaltungen + await loadSchaltungen(); + + // Only refresh provisions if the tab has been accessed before (performance optimization) + const provisionenTable = document.querySelector('#provisionenTable tbody'); + if (provisionenTable && provisionenTable.innerHTML.trim()) { + await loadProvisionen(); + } +} + +// Load Schaltungen overview +async function loadSchaltungen() { + try { + const schaltungen = await apiRequest('/api/schaltungen'); + const tbody = document.querySelector('#schaltungenTable tbody'); + + tbody.innerHTML = ''; + + schaltungen.forEach(s => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${s.schaltungs_id} + ${formatDate(s.eingabe_datum)} + ${formatDate(s.schaltungs_datum)} + ${s.vorname} ${s.nachname} + ${s.firma || '-'} + ${s.rufnummer} + ${s.anbieter_name} + ${s.tarif_name} + ${s.hardware} + + + + + + `; + tbody.appendChild(row); + }); + + } catch (error) { + console.error('Fehler beim Laden der Schaltungen:', error); + } +} + +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('de-DE'); +} + +async function showDocuments(schaltungId) { + try { + const documents = await apiRequest(`/api/schaltungen/${schaltungId}/dokumente`); + const modal = document.getElementById('documentsModal'); + const modalBody = document.getElementById('documentsModalBody'); + + modalBody.innerHTML = ''; + + if (documents.length === 0) { + modalBody.innerHTML = ` +
+
📄
+
Keine Dokumente vorhanden
+
Für diese Schaltung wurden noch keine Dokumente hochgeladen.
+
+ +
+
+ `; + } else { + documents.forEach(doc => { + const docItem = document.createElement('div'); + docItem.className = 'document-item'; + + // Get file extension for icon + const fileExt = doc.original_filename.split('.').pop().toLowerCase(); + let icon = '📄'; + if (['pdf'].includes(fileExt)) icon = '📋'; + else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) icon = '🖼️'; + else if (['doc', 'docx'].includes(fileExt)) icon = '📝'; + + // Format file size + const fileSize = doc.filesize ? `${(doc.filesize / 1024).toFixed(1)} KB` : 'Unbekannt'; + + // Format upload date + const uploadDate = doc.uploaded_at ? formatDate(doc.uploaded_at) : 'Unbekannt'; + + // Add view button for PDFs + const viewButton = fileExt === 'pdf' + ? `` + : ''; + + docItem.innerHTML = ` +
+
${icon}
+
+
${doc.original_filename}
+
Größe: ${fileSize} • Hochgeladen: ${uploadDate}
+
+
+
+ ${viewButton} + + +
+ `; + + modalBody.appendChild(docItem); + }); + } + + // Setup file upload handler for additional documents + const fileInput = document.getElementById('additionalDocumentUpload'); + fileInput.onchange = function(event) { + uploadAdditionalDocuments(schaltungId, event.target.files); + }; + + modal.classList.add('show'); + + } catch (error) { + console.error('Fehler beim Laden der Dokumente:', error); + showMessage('Fehler beim Laden der Dokumente', 'error'); + } +} + +function closeDocumentsModal() { + const modal = document.getElementById('documentsModal'); + modal.classList.remove('show'); +} + +function downloadDocument(documentId) { + window.open(`/api/dokumente/${documentId}/download`, '_blank'); +} + +// Upload additional documents +async function uploadAdditionalDocuments(schaltungId, files) { + if (!files || files.length === 0) return; + + const uploadPromises = Array.from(files).map(async (file) => { + const formData = new FormData(); + formData.append('document', file); + + try { + const response = await fetch(`/api/schaltungen/${schaltungId}/dokumente`, { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return { success: true, filename: result.filename }; + } catch (error) { + console.error(`Fehler beim Hochladen von ${file.name}:`, error); + return { success: false, filename: file.name, error: error.message }; + } + }); + + try { + const results = await Promise.all(uploadPromises); + + const successCount = results.filter(r => r.success).length; + const failureCount = results.filter(r => !r.success).length; + + if (successCount > 0) { + showMessage(`${successCount} Dokument(e) erfolgreich hochgeladen`, 'success'); + // Refresh the documents display + showDocuments(schaltungId); + } + + if (failureCount > 0) { + showMessage(`${failureCount} Dokument(e) konnten nicht hochgeladen werden`, 'error'); + } + + // Clear the file input + document.getElementById('additionalDocumentUpload').value = ''; + + } catch (error) { + console.error('Fehler beim Upload:', error); + showMessage('Fehler beim Hochladen der Dokumente', 'error'); + } +} + +// Delete document +async function deleteDocument(documentId, filename, schaltungId) { + if (!confirm(`Möchten Sie das Dokument "${filename}" wirklich löschen?`)) { + return; + } + + try { + const response = await fetch(`/api/dokumente/${documentId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + showMessage('Dokument erfolgreich gelöscht', 'success'); + + // Refresh the documents display + showDocuments(schaltungId); + + } catch (error) { + console.error('Fehler beim Löschen:', error); + showMessage('Fehler beim Löschen des Dokuments', 'error'); + } +} + +// Close modal when clicking outside +document.addEventListener('click', function(event) { + const modal = document.getElementById('documentsModal'); + if (event.target === modal) { + closeDocumentsModal(); + } +}); + +// Admin functions + +// New Admin Category Navigation +function showAdminCategory(category) { + // Hide all categories + document.querySelectorAll('.admin-category').forEach(cat => { + cat.classList.remove('active'); + }); + + // Remove active class from all buttons + document.querySelectorAll('.admin-cat-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + // Show selected category + document.getElementById(`admin-${category}`).classList.add('active'); + + // Add active class to clicked button + event.target.closest('.admin-cat-btn').classList.add('active'); + + // Load data for the category + switch(category) { + case 'anbieter': + loadAnbieterAdmin(); + break; + case 'tarife': + loadTarifeAdmin(); + loadAnbieterForTarife(); + break; + case 'tarifoptionen': + loadTarifoptionAnbieter(); + displayTarifoptionenAdmin(); + break; + case 'mitarbeiter-prozentsaetze': + loadMitarbeiterAnbieter(); + loadMitarbeiterProzentsaetzeAdmin(); + break; + case 'mitarbeiter-provisionen': + loadTarifProvisionsAdmin(); + break; + } +} + +async function loadAnbieterAdmin() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const tbody = document.getElementById('anbieterList'); + + tbody.innerHTML = ''; + + if (anbieter.length === 0) { + tbody.innerHTML = 'Keine Anbieter vorhanden'; + return; + } + + anbieter.forEach(a => { + // Format the date nicely + const createdAt = new Date(a.created_at).toLocaleDateString('de-DE'); + + const row = ` + + ${a.id} + ${a.name} + ${createdAt} + + + + + `; + tbody.innerHTML += row; + }); + + } catch (error) { + console.error('Fehler beim Laden der Anbieter-Administration:', error); + const tbody = document.getElementById('anbieterList'); + tbody.innerHTML = 'Fehler beim Laden der Daten'; + } +} + +// Prevent race conditions +let isLoadingTarifeAdmin = false; + +async function loadTarifeAdmin() { + if (isLoadingTarifeAdmin) { + console.warn('loadTarifeAdmin already running, skipping to prevent race condition'); + return; + } + + try { + isLoadingTarifeAdmin = true; + // Use new API endpoint that gets all tarife with anbieter names + const tarife = await apiRequest('/api/tarife'); + const tbody = document.getElementById('tarifeList'); + + tbody.innerHTML = ''; + + if (tarife.length === 0) { + tbody.innerHTML = 'Keine Tarife vorhanden'; + return; + } + + tarife.forEach(t => { + const provision = `${(t.provision_euro || 0).toFixed(2).replace('.', ',')}€`; + + const row = ` + + ${t.id} + ${t.anbieter_name || 'Unbekannt'} + ${t.name} + ${provision} + ${t.beschreibung || '-'} + + + + + + `; + tbody.innerHTML += row; + }); + + } catch (error) { + console.error('Fehler beim Laden der Tarif-Administration:', error); + const tbody = document.getElementById('tarifeList'); + tbody.innerHTML = 'Fehler beim Laden der Daten'; + } finally { + isLoadingTarifeAdmin = false; + } +} + +async function addAnbieter() { + const name = document.getElementById('newAnbieter').value.trim(); + + if (!name) { + showMessage('Bitte einen Anbieter-Namen eingeben', 'error'); + return; + } + + try { + await apiRequest('/api/anbieter', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ name }) + }); + + document.getElementById('newAnbieter').value = ''; + showMessage('Anbieter erfolgreich hinzugefügt'); + + // Reload data + await loadAnbieter(); + await loadAnbieterAdmin(); + + } catch (error) { + console.error('Fehler beim Hinzufügen des Anbieters:', error); + } +} + +// Load anbieter for tarife select +async function loadAnbieterForTarife() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const select = document.getElementById('tarifAnbieter'); + + select.innerHTML = ''; + anbieter.forEach(a => { + select.innerHTML += ``; + }); + } catch (error) { + console.error('Fehler beim Laden der Anbieter für Tarife:', error); + } +} + +// Load anbieter for mitarbeiter select +async function loadMitarbeiterAnbieter() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const select = document.getElementById('mitarbeiterAnbieter'); + + select.innerHTML = ''; + anbieter.forEach(a => { + select.innerHTML += ``; + }); + } catch (error) { + console.error('Fehler beim Laden der Anbieter für Mitarbeiter:', error); + } +} + +// Load anbieter for tarifoption select +async function loadTarifoptionAnbieter() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const select = document.getElementById('tarifoptionAnbieter'); + + select.innerHTML = ''; + anbieter.forEach(a => { + select.innerHTML += ``; + }); + } catch (error) { + console.error('Fehler beim Laden der Anbieter für Tarifoptionen:', error); + } +} + +async function addTarif() { + const name = document.getElementById('newTarif').value.trim(); + const anbieterId = document.getElementById('tarifAnbieter').value; + const provisionEuro = document.getElementById('tarifProvisionBetrag').value; + const beschreibung = document.getElementById('tarifBeschreibung').value.trim(); + + if (!name || !anbieterId) { + showMessage('Bitte Tarif-Name und Anbieter auswählen', 'error'); + return; + } + + // Validation for provision euro + if (provisionEuro && parseFloat(provisionEuro) < 0) { + showMessage('Provision kann nicht negativ sein', 'error'); + return; + } + + try { + const requestData = { + name, + anbieter_id: anbieterId, + provision_euro: provisionEuro ? parseFloat(provisionEuro) : 0, + beschreibung: beschreibung || null + }; + + await apiRequest('/api/tarife', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }); + + // Reset form + document.getElementById('newTarif').value = ''; + document.getElementById('tarifAnbieter').value = ''; + document.getElementById('tarifProvisionBetrag').value = ''; + document.getElementById('tarifBeschreibung').value = ''; + + showMessage('Tarif erfolgreich hinzugefügt'); + + // Reload data + await loadTarifeAdmin(); + await loadTarifeForProvision(); + + } catch (error) { + console.error('Fehler beim Hinzufügen des Tarifs:', error); + showMessage('Fehler beim Hinzufügen des Tarifs', 'error'); + } +} + +// Commission management functions + +async function loadProvisionen() { + try { + const data = await apiRequest('/api/provisionen'); + + // Store data globally for filtering + allProvisionen = data; + + // Update filter dropdowns + await updateFilterDropdowns(); + + // Apply current filters (which will also update the display) + applyFiltersProvisionen(); + + } catch (error) { + console.error('Fehler beim Laden der Provisionen:', error); + } +} + +async function loadAdminData() { + await loadAnbieterAdmin(); + await loadTarifeAdmin(); + await loadGlobalProvisionsregelnAdmin(); + await loadProvisionsregelnAdmin(); + await loadTarifeForProvision(); + await loadGlobalProvisionAnbieter(); + await loadTarifoptionenAdmin(); +} + +async function loadTarifeForProvision() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const provisionTarifSelect = document.getElementById('provisionTarif'); + + if (!provisionTarifSelect) return; + + provisionTarifSelect.innerHTML = ''; + + for (const a of anbieter) { + const tarife = await apiRequest(`/api/tarife/${a.id}`); + if (tarife.length > 0) { + tarife.forEach(t => { + provisionTarifSelect.innerHTML += ``; + }); + } + } + + } catch (error) { + console.error('Fehler beim Laden der Tarife für Provisionen:', error); + } +} + +async function loadProvisionsregelnAdmin() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const provisionsregelnList = document.getElementById('provisionsregelnList'); + + if (!provisionsregelnList) return; + + provisionsregelnList.innerHTML = ''; + + for (const a of anbieter) { + const tarife = await apiRequest(`/api/tarife/${a.id}`); + + for (const t of tarife) { + const regeln = await apiRequest(`/api/tarife/${t.id}/provisionsregeln`); + + if (regeln.length > 0) { + const providerDiv = document.createElement('div'); + providerDiv.innerHTML = `

${a.name} - ${t.name}

`; + provisionsregelnList.appendChild(providerDiv); + + regeln.forEach(r => { + const item = document.createElement('div'); + item.className = 'list-item'; + item.style.marginLeft = '20px'; + + const typText = r.provision_typ === 'fix' ? `${r.betrag} €` : `${r.betrag} %`; + const statusText = r.aktiv ? 'Aktiv' : 'Inaktiv'; + + item.innerHTML = ` + ${typText} - ${r.beschreibung || 'Keine Beschreibung'} (${statusText}) +
+ +
+ `; + provisionsregelnList.appendChild(item); + }); + } + } + } + + } catch (error) { + console.error('Fehler beim Laden der Provisionsregeln-Administration:', error); + } +} + +async function addProvisionsregel() { + const tarifId = document.getElementById('provisionTarif').value; + const typ = document.getElementById('provisionTyp').value; + const betrag = document.getElementById('provisionBetrag').value; + const beschreibung = document.getElementById('provisionBeschreibung').value; + + if (!tarifId || !typ || !betrag) { + showMessage('Bitte alle Pflichtfelder ausfüllen', 'error'); + return; + } + + if (typ === 'prozent' && (parseFloat(betrag) < 0 || parseFloat(betrag) > 100)) { + showMessage('Prozentual muss zwischen 0 und 100 sein', 'error'); + return; + } + + if (parseFloat(betrag) < 0) { + showMessage('Betrag kann nicht negativ sein', 'error'); + return; + } + + try { + await apiRequest('/api/provisionsregeln', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + tarif_id: tarifId, + provision_typ: typ, + betrag: parseFloat(betrag), + beschreibung: beschreibung || null + }) + }); + + // Reset form + document.getElementById('provisionTarif').value = ''; + document.getElementById('provisionTyp').value = ''; + document.getElementById('provisionBetrag').value = ''; + document.getElementById('provisionBeschreibung').value = ''; + + showMessage('Provisionsregel erfolgreich hinzugefügt'); + + // Reload data + await loadProvisionsregelnAdmin(); + + } catch (error) { + console.error('Fehler beim Hinzufügen der Provisionsregel:', error); + } +} + +async function toggleProvisionsregel(id, aktiv) { + try { + await apiRequest(`/api/provisionsregeln/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + aktiv: aktiv + }) + }); + + showMessage(`Provisionsregel erfolgreich ${aktiv ? 'aktiviert' : 'deaktiviert'}`); + + // Reload data + await loadProvisionsregelnAdmin(); + + } catch (error) { + console.error('Fehler beim Ändern der Provisionsregel:', error); + showMessage('Fehler beim Ändern der Provisionsregel', 'error'); + } +} + +// Global Provider Commission Rules Functions + +async function loadGlobalProvisionAnbieter() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const globalProvisionAnbieterSelect = document.getElementById('globalProvisionAnbieter'); + + if (!globalProvisionAnbieterSelect) return; + + globalProvisionAnbieterSelect.innerHTML = ''; + + anbieter.forEach(a => { + globalProvisionAnbieterSelect.innerHTML += ``; + }); + + } catch (error) { + console.error('Fehler beim Laden der Anbieter für globale Provisionen:', error); + } +} + +async function loadGlobalProvisionsregelnAdmin() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const globalProvisionsregelnList = document.getElementById('globalProvisionsregelnList'); + + if (!globalProvisionsregelnList) return; + + globalProvisionsregelnList.innerHTML = ''; + + for (const a of anbieter) { + const regeln = await apiRequest(`/api/anbieter/${a.id}/provisionsregeln`); + + if (regeln.length > 0) { + const providerDiv = document.createElement('div'); + providerDiv.innerHTML = `

Globale Regeln für ${a.name}

`; + globalProvisionsregelnList.appendChild(providerDiv); + + regeln.forEach(r => { + const item = document.createElement('div'); + item.className = 'list-item'; + item.style.marginLeft = '20px'; + + // Show employee commission instead of shop commission + let displayText = 'Keine Mitarbeiter-Provision definiert'; + if (r.verkaeufer_provision_typ && r.verkaeufer_provision_betrag) { + const mitarbeiterText = r.verkaeufer_provision_typ === 'fix' + ? `${r.verkaeufer_provision_betrag} €` + : `${r.verkaeufer_provision_betrag} %`; + displayText = `Mitarbeiter: ${mitarbeiterText}`; + if (r.verkaeufer_provision_beschreibung) { + displayText += ` - ${r.verkaeufer_provision_beschreibung}`; + } + } + + const statusText = r.aktiv ? 'Aktiv' : 'Inaktiv'; + + item.innerHTML = ` + ${displayText} (${statusText}) +
+ +
+ `; + globalProvisionsregelnList.appendChild(item); + }); + } + } + + } catch (error) { + console.error('Fehler beim Laden der globalen Provisionsregeln-Administration:', error); + } +} + +async function addGlobalProvisionsregel() { + const anbieterId = document.getElementById('globalProvisionAnbieter').value; + + // Employee provision fields + const verkaeuferProvisionTyp = document.getElementById('globalVerkaeuferProvisionTyp').value; + const verkaeuferProvisionBetrag = document.getElementById('globalVerkaeuferProvisionBetrag').value; + const verkaeuferProvisionBeschreibung = document.getElementById('globalVerkaeuferProvisionBeschreibung').value; + + if (!anbieterId || !verkaeuferProvisionTyp || !verkaeuferProvisionBetrag) { + showMessage('Bitte alle Pflichtfelder ausfüllen', 'error'); + return; + } + + if (verkaeuferProvisionTyp === 'prozent' && (parseFloat(verkaeuferProvisionBetrag) < 0 || parseFloat(verkaeuferProvisionBetrag) > 100)) { + showMessage('Mitarbeiter-Provision Prozentual muss zwischen 0 und 100 sein', 'error'); + return; + } + + if (parseFloat(verkaeuferProvisionBetrag) < 0) { + showMessage('Mitarbeiter-Provision kann nicht negativ sein', 'error'); + return; + } + + try { + const requestData = { + anbieter_id: anbieterId, + // For employee-only rules, we need a meaningful shop provision as base + provision_typ: 'prozent', // Use percentage as it's most flexible + betrag: 100, // 100% - this means shop gets the full tariff commission + beschreibung: 'Standard Anbieter-Provision (100% der Tarif-Provision)', + verkaeufer_provision_typ: verkaeuferProvisionTyp, + verkaeufer_provision_betrag: parseFloat(verkaeuferProvisionBetrag), + verkaeufer_provision_beschreibung: verkaeuferProvisionBeschreibung || null + }; + + await apiRequest('/api/anbieter-provisionsregeln', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }); + + // Reset form + document.getElementById('globalProvisionAnbieter').value = ''; + document.getElementById('globalVerkaeuferProvisionTyp').value = ''; + document.getElementById('globalVerkaeuferProvisionBetrag').value = ''; + document.getElementById('globalVerkaeuferProvisionBeschreibung').value = ''; + + showMessage('Globale Mitarbeiter-Provisionsregel erfolgreich hinzugefügt'); + + // Reload data + await loadGlobalProvisionsregelnAdmin(); + + } catch (error) { + console.error('Fehler beim Hinzufügen der globalen Provisionsregel:', error); + } +} + +async function toggleGlobalProvisionsregel(id, aktiv) { + try { + await apiRequest(`/api/anbieter-provisionsregeln/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + aktiv: aktiv + }) + }); + + showMessage(`Globale Provisionsregel erfolgreich ${aktiv ? 'aktiviert' : 'deaktiviert'}`); + + // Reload data + await loadGlobalProvisionsregelnAdmin(); + + } catch (error) { + console.error('Fehler beim Ändern der globalen Provisionsregel:', error); + showMessage('Fehler beim Ändern der globalen Provisionsregel', 'error'); + } +} + +// Edit/Delete Functions for Tariffs and Schaltungen + +// Modal Management +function closeModal(modalId) { + document.getElementById(modalId).style.display = 'none'; +} + +// Tariff Edit/Delete Functions +async function editTariff(id) { + try { + // Get all tariffs from the provider + const anbieter = await apiRequest('/api/anbieter'); + let tariff = null; + + for (const a of anbieter) { + const tarife = await apiRequest(`/api/tarife/${a.id}`); + tariff = tarife.find(t => t.id == id); + if (tariff) break; + } + + if (!tariff) { + showMessage('Tarif nicht gefunden', 'error'); + return; + } + + // Fill modal with tariff data + document.getElementById('editTariffId').value = tariff.id; + document.getElementById('editTariffName').value = tariff.name; + document.getElementById('editTariffProvisionBetrag').value = tariff.provision_euro || ''; + + + // Show modal + document.getElementById('editTariffModal').style.display = 'block'; + + // Setup form submission + document.getElementById('editTariffForm').onsubmit = async (e) => { + e.preventDefault(); + + const formData = { + name: document.getElementById('editTariffName').value.trim(), + provision_euro: document.getElementById('editTariffProvisionBetrag').value ? parseFloat(document.getElementById('editTariffProvisionBetrag').value) : 0 + }; + + try { + await apiRequest(`/api/tarife/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + closeModal('editTariffModal'); + showMessage('Tarif erfolgreich aktualisiert'); + await loadTarifeAdmin(); + await loadTarifeForProvision(); + } catch (error) { + console.error('Fehler beim Aktualisieren des Tarifs:', error); + } + }; + + } catch (error) { + console.error('Fehler beim Laden der Tarif-Daten:', error); + } +} + +async function deleteTariff(id, name) { + if (!confirm(`Sind Sie sicher, dass Sie den Tarif "${name}" löschen möchten?\n\nHinweis: Tarife mit bestehenden Schaltungen können nicht gelöscht werden.`)) { + return; + } + + try { + await apiRequest(`/api/tarife/${id}`, { + method: 'DELETE' + }); + + showMessage('Tarif erfolgreich gelöscht'); + // Remove the deleted item from DOM immediately (much faster) + const deletedElement = document.getElementById(`tarif-item-${id}`); + if (deletedElement) { + deletedElement.remove(); + } + + } catch (error) { + console.error('Fehler beim Löschen des Tarifs:', error); + } +} + +async function deleteAnbieter(id, name) { + if (!confirm(`Sind Sie sicher, dass Sie den Anbieter "${name}" löschen möchten?\n\nHinweis: Anbieter mit bestehenden Tarifen oder Schaltungen können nicht gelöscht werden.`)) { + return; + } + + try { + await apiRequest(`/api/anbieter/${id}`, { + method: 'DELETE' + }); + + showMessage('Anbieter erfolgreich gelöscht'); + await loadAnbieterAdmin(); + await loadAnbieter(); + await loadAnbieterForProvision(); + + } catch (error) { + console.error('Fehler beim Löschen des Anbieters:', error); + + // Show more detailed error message if provided by server + if (error.message) { + showMessage(error.message, 'error'); + } + } +} + +// Schaltung Edit/Delete Functions +async function editSchaltung(id) { + try { + // Get schaltung data + const schaltungen = await apiRequest('/api/schaltungen'); + const schaltung = schaltungen.find(s => s.id == id); + + if (!schaltung) { + showMessage('Schaltung nicht gefunden', 'error'); + return; + } + + // Load anbieter for the modal + const anbieter = await apiRequest('/api/anbieter'); + const editAnbieterSelect = document.getElementById('editAnbieter'); + editAnbieterSelect.innerHTML = ''; + + anbieter.forEach(a => { + editAnbieterSelect.innerHTML += ``; + }); + + // Fill modal with schaltung data + document.getElementById('editSchaltungId').value = schaltung.id; + document.getElementById('editSchaltungsId').value = schaltung.schaltungs_id; + document.getElementById('editEingabeDatum').value = schaltung.eingabe_datum; + document.getElementById('editSchaltungsDatum').value = schaltung.schaltungs_datum; + document.getElementById('editVorname').value = schaltung.vorname; + document.getElementById('editNachname').value = schaltung.nachname; + document.getElementById('editFirma').value = schaltung.firma || ''; + document.getElementById('editRufnummer').value = schaltung.rufnummer; + document.getElementById('editHardware').value = schaltung.hardware || ''; + document.getElementById('editHardwareEk').value = schaltung.hardware_ek || ''; + document.getElementById('editKundenverkaufspreis').value = schaltung.kundenverkaufspreis_netto || ''; + + // Toggle hardware pricing visibility + toggleEditHardwarePricing(); + + // Set anbieter and load tarife + editAnbieterSelect.value = schaltung.anbieter_id; + await loadTarifeForEdit(); + document.getElementById('editTarif').value = schaltung.tarif_id; + + // Load tarifoptionen for the selected tarif + await loadTarifoptionenForEdit(); + + // Load and check currently selected tarifoptionen + try { + const currentTarifoptionen = await apiRequest(`/api/schaltungen/${id}/tarifoptionen`); + console.log('Current tarifoptionen for editing:', currentTarifoptionen); + console.log('First option detailed:', JSON.stringify(currentTarifoptionen[0], null, 2)); + + // Store current tarifoptionen for later use + window.currentEditTarifoptionen = currentTarifoptionen; + + // Wait a bit to ensure tarifoptionen are loaded + setTimeout(() => { + // Debug: Show all available checkboxes + const allCheckboxes = document.querySelectorAll('input[name="editTarifoptionen"]'); + console.log('All available checkboxes:', Array.from(allCheckboxes).map(cb => cb.value)); + + // Check the checkboxes for currently selected options + currentTarifoptionen.forEach(option => { + // Try both possible field names + const optionId = option.tarifoption_id || option.id; + console.log(`Looking for checkbox with value: ${optionId} (from tarifoption_id: ${option.tarifoption_id}, id: ${option.id})`); + const checkbox = document.querySelector(`input[name="editTarifoptionen"][value="${optionId}"]`); + if (checkbox) { + console.log(`Found checkbox, checking it`); + checkbox.checked = true; + // Add visual indicator for currently selected options + const label = checkbox.closest('label'); + if (label) { + label.style.backgroundColor = '#e8f5e8'; + label.style.padding = '5px'; + label.style.borderRadius = '3px'; + label.style.fontWeight = 'bold'; + } + } else { + console.log(`Checkbox not found for tarifoption_id: ${optionId}`); + } + }); + }, 100); + } catch (error) { + console.error('Fehler beim Laden der aktuellen Tarifoptionen:', error); + } + + // Show modal + document.getElementById('editSchaltungModal').style.display = 'block'; + + // Setup form submission + document.getElementById('editSchaltungForm').onsubmit = async (e) => { + e.preventDefault(); + + const hardware = document.getElementById('editHardware').value.trim(); + const hardwareEk = document.getElementById('editHardwareEk').value; + const kundenverkaufspreis = document.getElementById('editKundenverkaufspreis').value; + + // Validate hardware pricing + if (hardware && hardware !== '') { + if (!hardwareEk || !kundenverkaufspreis) { + showMessage('Wenn Hardware eingegeben wird, müssen Hardware-EK und Kundenverkaufspreis ausgefüllt werden', 'error'); + return; + } + } + + // Get selected tarifoptionen + const selectedTarifoptionen = []; + const checkboxes = document.querySelectorAll('input[name="editTarifoptionen"]:checked'); + checkboxes.forEach(checkbox => { + selectedTarifoptionen.push(parseInt(checkbox.value)); + }); + + const formData = { + schaltungs_id: document.getElementById('editSchaltungsId').value.trim(), + eingabe_datum: document.getElementById('editEingabeDatum').value, + schaltungs_datum: document.getElementById('editSchaltungsDatum').value, + vorname: document.getElementById('editVorname').value.trim(), + nachname: document.getElementById('editNachname').value.trim(), + firma: document.getElementById('editFirma').value.trim() || null, + rufnummer: document.getElementById('editRufnummer').value.trim(), + anbieter_id: parseInt(document.getElementById('editAnbieter').value), + tarif_id: parseInt(document.getElementById('editTarif').value), + hardware: hardware || null, + hardware_ek: hardware && hardwareEk ? parseFloat(hardwareEk) : null, + kundenverkaufspreis_netto: hardware && kundenverkaufspreis ? parseFloat(kundenverkaufspreis) : null, + tarifoptionen: selectedTarifoptionen + }; + + try { + await apiRequest(`/api/schaltungen/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + closeModal('editSchaltungModal'); + showMessage('Schaltung erfolgreich aktualisiert'); + await refreshDataAfterSchaltungChange(); + } catch (error) { + console.error('Fehler beim Aktualisieren der Schaltung:', error); + } + }; + + } catch (error) { + console.error('Fehler beim Laden der Schaltungs-Daten:', error); + } +} + +function toggleEditHardwarePricing() { + const hardwareInput = document.getElementById('editHardware'); + const pricingFields = document.getElementById('editHardwarePricingFields'); + const hardwareEkInput = document.getElementById('editHardwareEk'); + const kundenverkaufspreisInput = document.getElementById('editKundenverkaufspreis'); + + if (hardwareInput.value.trim() !== '') { + pricingFields.style.display = 'block'; + hardwareEkInput.required = true; + kundenverkaufspreisInput.required = true; + } else { + pricingFields.style.display = 'none'; + hardwareEkInput.required = false; + kundenverkaufspreisInput.required = false; + hardwareEkInput.value = ''; + kundenverkaufspreisInput.value = ''; + } +} + +async function loadTarifeForEdit() { + const anbieterId = document.getElementById('editAnbieter').value; + const tarifSelect = document.getElementById('editTarif'); + + if (!anbieterId) { + tarifSelect.innerHTML = ''; + document.getElementById('editTarifoptionenContainer').innerHTML = '
Erst Anbieter wählen...
'; + return; + } + + try { + const tarife = await apiRequest(`/api/tarife/${anbieterId}`); + + tarifSelect.innerHTML = ''; + tarife.forEach(t => { + tarifSelect.innerHTML += ``; + }); + + // Clear tarifoptionen when anbieter changes + document.getElementById('editTarifoptionenContainer').innerHTML = '
Erst Tarif wählen...
'; + + // Add event listener for tarif change + tarifSelect.onchange = loadTarifoptionenForEdit; + + } catch (error) { + console.error('Fehler beim Laden der Tarife für Bearbeitung:', error); + tarifSelect.innerHTML = ''; + } +} + +async function loadTarifoptionenForEdit() { + const tarifId = document.getElementById('editTarif').value; + const container = document.getElementById('editTarifoptionenContainer'); + + if (!tarifId) { + container.innerHTML = '
Erst Tarif wählen...
'; + return; + } + + try { + const tarifoptionen = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`); + + if (tarifoptionen.length === 0) { + container.innerHTML = '
Keine Tarifoptionen verfügbar
'; + return; + } + + container.innerHTML = ''; + tarifoptionen.forEach(option => { + const optionDiv = document.createElement('div'); + optionDiv.style.marginBottom = '5px'; + + const label = document.createElement('label'); + label.style.fontWeight = 'normal'; + label.style.display = 'block'; + label.style.cursor = 'pointer'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'editTarifoptionen'; + checkbox.value = option.id; + + // Add change event to handle visual feedback + checkbox.addEventListener('change', function() { + if (this.checked) { + label.style.backgroundColor = '#e8f5e8'; + label.style.padding = '5px'; + label.style.borderRadius = '3px'; + label.style.fontWeight = 'bold'; + } else { + // Check if this was originally selected + const wasOriginallySelected = window.currentEditTarifoptionen && + window.currentEditTarifoptionen.some(current => (current.tarifoption_id || current.id) == option.id); + + if (!wasOriginallySelected) { + label.style.backgroundColor = ''; + label.style.padding = ''; + label.style.borderRadius = ''; + label.style.fontWeight = 'normal'; + } else { + // Keep highlighting but show it's deselected + label.style.backgroundColor = '#ffe8e8'; + label.style.padding = '5px'; + label.style.borderRadius = '3px'; + label.style.fontWeight = 'bold'; + } + } + }); + + label.appendChild(checkbox); + label.appendChild(document.createTextNode(' ' + option.name + ' ')); + + const priceSpan = document.createElement('span'); + priceSpan.style.color = '#666'; + priceSpan.textContent = `(${option.provision_euro >= 0 ? '+' : ''}${option.provision_euro}€)`; + label.appendChild(priceSpan); + + optionDiv.appendChild(label); + container.appendChild(optionDiv); + }); + + } catch (error) { + console.error('Fehler beim Laden der Tarifoptionen für Bearbeitung:', error); + container.innerHTML = '
Fehler beim Laden der Tarifoptionen
'; + } +} + +function deleteSchaltung(id, schaltungsId, name) { + document.getElementById('deleteSchaltungId').value = id; + document.getElementById('deleteSchaltungInfo').textContent = `${schaltungsId} - ${name}`; + document.getElementById('deleteReason').value = ''; + document.getElementById('deleteSchaltungModal').style.display = 'block'; +} + +async function confirmDeleteSchaltung() { + const id = document.getElementById('deleteSchaltungId').value; + const reason = document.getElementById('deleteReason').value.trim(); + + if (!reason) { + showMessage('Bitte geben Sie einen Löschgrund ein', 'error'); + return; + } + + try { + await apiRequest(`/api/schaltungen/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ loeschgrund: reason }) + }); + + closeModal('deleteSchaltungModal'); + showMessage('Schaltung erfolgreich gelöscht und archiviert'); + await refreshDataAfterSchaltungChange(); + + } catch (error) { + console.error('Fehler beim Löschen der Schaltung:', error); + } +} + +// Load Deleted Schaltungen +async function loadGeloeschteSchaltungen() { + try { + const geloeschte = await apiRequest('/api/geloeschte-schaltungen'); + const tbody = document.querySelector('#geloeschteSchaltungenTable tbody'); + + tbody.innerHTML = ''; + + geloeschte.forEach(s => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${s.schaltungs_id} + ${formatDate(s.eingabe_datum)} + ${formatDate(s.schaltungs_datum)} + ${s.vorname} ${s.nachname} + ${s.firma || '-'} + ${s.rufnummer} + ${s.anbieter_name} + ${s.tarif_name} + ${s.hardware} + ${s.loeschgrund} + ${formatDateTime(s.geloescht_am)} + `; + tbody.appendChild(row); + }); + + } catch (error) { + console.error('Fehler beim Laden der gelöschten Schaltungen:', error); + } +} + +function formatDateTime(dateTimeString) { + const date = new Date(dateTimeString); + return date.toLocaleDateString('de-DE') + ' ' + date.toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}); +} + +// Filter functionality +let allSchaltungen = []; +let allProvisionen = []; + +// Modified loadSchaltungen to store data and setup filters +const originalLoadSchaltungen = loadSchaltungen; +loadSchaltungen = async function() { + try { + const data = await apiRequest('/api/schaltungen'); + allSchaltungen = data; + + // Update filter dropdowns + await updateFilterDropdowns(); + + // Apply current filters + applyFiltersOverview(); + + } catch (error) { + console.error('Fehler beim Laden der Schaltungen:', error); + } +}; + + +// Update filter dropdowns from administration data +async function updateFilterDropdowns() { + try { + // Get all anbieter from administration + const anbieter = await apiRequest('/api/anbieter'); + + // Update Overview anbieter filter + const anbieterOverview = document.getElementById('filterAnbieterOverview'); + if (anbieterOverview) { + anbieterOverview.innerHTML = ''; + anbieter.forEach(a => { + anbieterOverview.innerHTML += ``; + }); + } + + // Update Provisionen anbieter filter + const anbieterProvisionen = document.getElementById('filterAnbieterProvisionen'); + if (anbieterProvisionen) { + anbieterProvisionen.innerHTML = ''; + anbieter.forEach(a => { + anbieterProvisionen.innerHTML += ``; + }); + } + + // Get all tarife from all anbieter + const tarifOverview = document.getElementById('filterTarifOverview'); + const tarifProvisionen = document.getElementById('filterTarifProvisionen'); + + if (tarifOverview) { + tarifOverview.innerHTML = ''; + } + if (tarifProvisionen) { + tarifProvisionen.innerHTML = ''; + } + + // Load tarife for each anbieter + for (const a of anbieter) { + try { + const tarife = await apiRequest(`/api/tarife/${a.id}`); + tarife.forEach(t => { + const optionText = `${a.name} - ${t.name}`; + const optionValue = t.name; + + if (tarifOverview) { + tarifOverview.innerHTML += ``; + } + if (tarifProvisionen) { + tarifProvisionen.innerHTML += ``; + } + }); + } catch (error) { + console.warn(`Fehler beim Laden der Tarife für Anbieter ${a.name}:`, error); + } + } + + } catch (error) { + console.error('Fehler beim Laden der Filter-Daten:', error); + } +} + +// Apply filters for Overview tab +function applyFiltersOverview() { + const anbieterFilter = document.getElementById('filterAnbieterOverview')?.value || ''; + const tarifFilter = document.getElementById('filterTarifOverview')?.value || ''; + const schaltungsIdFilter = document.getElementById('filterSchaltungsIdOverview')?.value || ''; + const rufnummerFilter = document.getElementById('filterRufnummerOverview')?.value || ''; + const datumVon = document.getElementById('filterDatumVonOverview')?.value || ''; + const datumBis = document.getElementById('filterDatumBisOverview')?.value || ''; + + let filteredData = allSchaltungen.filter(schaltung => { + // Anbieter filter + if (anbieterFilter && schaltung.anbieter_name !== anbieterFilter) { + return false; + } + + // Tarif filter + if (tarifFilter && schaltung.tarif_name !== tarifFilter) { + return false; + } + + // Schaltungs-ID filter (supports multiple comma-separated IDs) + if (schaltungsIdFilter) { + const schaltungsIds = schaltungsIdFilter.split(',').map(id => id.trim().toLowerCase()).filter(id => id.length > 0); + if (schaltungsIds.length > 0) { + const schaltungsIdLower = schaltung.schaltungs_id.toLowerCase(); + const matches = schaltungsIds.some(searchId => schaltungsIdLower.includes(searchId)); + if (!matches) { + return false; + } + } + } + + // Rufnummer filter (case insensitive partial match) + if (rufnummerFilter && !schaltung.rufnummer.toLowerCase().includes(rufnummerFilter.toLowerCase())) { + return false; + } + + // Date range filter + const schaltungsDatum = new Date(schaltung.schaltungs_datum); + if (datumVon && schaltungsDatum < new Date(datumVon)) { + return false; + } + if (datumBis && schaltungsDatum > new Date(datumBis)) { + return false; + } + + return true; + }); + + // Update table with filtered data + updateSchaltungenTable(filteredData); +} + +// Apply filters for Provisionen tab +function applyFiltersProvisionen() { + const anbieterFilter = document.getElementById('filterAnbieterProvisionen')?.value || ''; + const tarifFilter = document.getElementById('filterTarifProvisionen')?.value || ''; + const schaltungsIdFilter = document.getElementById('filterSchaltungsIdProvisionen')?.value || ''; + const rufnummerFilter = document.getElementById('filterRufnummerProvisionen')?.value || ''; + const datumVon = document.getElementById('filterDatumVonProvisionen')?.value || ''; + const datumBis = document.getElementById('filterDatumBisProvisionen')?.value || ''; + + let filteredSchaltungen = allProvisionen.schaltungen.filter(schaltung => { + // Anbieter filter + if (anbieterFilter && schaltung.anbieter_name !== anbieterFilter) { + return false; + } + + // Tarif filter + if (tarifFilter && schaltung.tarif_name !== tarifFilter) { + return false; + } + + // Schaltungs-ID filter (supports multiple comma-separated IDs) + if (schaltungsIdFilter) { + const schaltungsIds = schaltungsIdFilter.split(',').map(id => id.trim().toLowerCase()).filter(id => id.length > 0); + if (schaltungsIds.length > 0) { + const schaltungsIdLower = schaltung.schaltungs_id.toLowerCase(); + const matches = schaltungsIds.some(searchId => schaltungsIdLower.includes(searchId)); + if (!matches) { + return false; + } + } + } + + // Rufnummer filter (case insensitive partial match) + if (rufnummerFilter && !schaltung.rufnummer.toLowerCase().includes(rufnummerFilter.toLowerCase())) { + return false; + } + + // Date range filter + const schaltungsDatum = new Date(schaltung.schaltungs_datum); + if (datumVon && schaltungsDatum < new Date(datumVon)) { + return false; + } + if (datumBis && schaltungsDatum > new Date(datumBis)) { + return false; + } + + return true; + }); + + // Recalculate totals for filtered data + const totalShopProvision = filteredSchaltungen.reduce((sum, row) => { + return sum + parseFloat(row.berechnete_shop_provision || 0); + }, 0); + + const totalMitarbeiterProvision = filteredSchaltungen.reduce((sum, row) => { + return sum + parseFloat(row.berechnete_mitarbeiter_provision || 0); + }, 0); + + // Update table and totals with filtered data + updateProvisionenTable(filteredSchaltungen, totalShopProvision, totalMitarbeiterProvision); +} + +// Update Schaltungen table with filtered data +function updateSchaltungenTable(schaltungen) { + const tbody = document.querySelector('#schaltungenTable tbody'); + if (!tbody) return; + + tbody.innerHTML = ''; + + if (schaltungen.length === 0) { + // Show "no results" message + const row = document.createElement('tr'); + row.innerHTML = ` + + 📋 Keine Schaltungen entsprechen den gewählten Filterkriterien + + `; + tbody.appendChild(row); + return; + } + + schaltungen.forEach(s => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${s.schaltungs_id} + ${formatDate(s.eingabe_datum)} + ${formatDate(s.schaltungs_datum)} + ${s.vorname} ${s.nachname} + ${s.firma || '-'} + ${s.rufnummer} + ${s.anbieter_name} + ${s.tarif_name} + ${s.hardware} + + + + + + + + `; + tbody.appendChild(row); + }); +} + +// Update Provisionen table with filtered data +function updateProvisionenTable(schaltungen, totalShopProvision, totalMitarbeiterProvision) { + const tbody = document.querySelector('#provisionenTable tbody'); + const totalShopSpan = document.getElementById('totalShopProvision'); + const totalMitarbeiterSpan = document.getElementById('totalMitarbeiterProvision'); + + if (!tbody) return; + + tbody.innerHTML = ''; + + if (schaltungen.length === 0) { + // Show "no results" message + const row = document.createElement('tr'); + row.innerHTML = ` + + 💰 Keine Provisionen entsprechen den gewählten Filterkriterien + + `; + tbody.appendChild(row); + + // Reset totals to 0 + if (totalShopSpan) totalShopSpan.textContent = '0,00 €'; + if (totalMitarbeiterSpan) totalMitarbeiterSpan.textContent = '0,00 €'; + return; + } + + schaltungen.forEach(s => { + const row = document.createElement('tr'); + + row.innerHTML = ` + ${s.schaltungs_id} + ${formatDate(s.schaltungs_datum)} + ${s.vorname} ${s.nachname} + ${s.anbieter_name} + ${s.tarif_name} + ${s.berechnete_shop_provision || '0.00'} € + ${s.berechnete_mitarbeiter_provision || '0.00'} € + `; + tbody.appendChild(row); + }); + + // Update totals + if (totalShopSpan) totalShopSpan.textContent = `${totalShopProvision.toFixed(2)} €`; + if (totalMitarbeiterSpan) totalMitarbeiterSpan.textContent = `${totalMitarbeiterProvision.toFixed(2)} €`; +} + +// Clear filters functions +function clearFiltersOverview() { + document.getElementById('filterAnbieterOverview').value = ''; + document.getElementById('filterTarifOverview').value = ''; + document.getElementById('filterSchaltungsIdOverview').value = ''; + document.getElementById('filterRufnummerOverview').value = ''; + document.getElementById('filterDatumVonOverview').value = ''; + document.getElementById('filterDatumBisOverview').value = ''; + applyFiltersOverview(); +} + +function clearFiltersProvisionen() { + document.getElementById('filterAnbieterProvisionen').value = ''; + document.getElementById('filterTarifProvisionen').value = ''; + document.getElementById('filterSchaltungsIdProvisionen').value = ''; + document.getElementById('filterRufnummerProvisionen').value = ''; + document.getElementById('filterDatumVonProvisionen').value = ''; + document.getElementById('filterDatumBisProvisionen').value = ''; + applyFiltersProvisionen(); +} + +// Add event listeners for filter changes +document.addEventListener('DOMContentLoaded', function() { + // Overview filters + const overviewFilters = [ + 'filterAnbieterOverview', + 'filterTarifOverview', + 'filterSchaltungsIdOverview', + 'filterRufnummerOverview', + 'filterDatumVonOverview', + 'filterDatumBisOverview' + ]; + + overviewFilters.forEach(filterId => { + const element = document.getElementById(filterId); + if (element) { + if (element.type === 'text') { + element.addEventListener('input', applyFiltersOverview); + } else { + element.addEventListener('change', applyFiltersOverview); + } + } + }); + + // Provisionen filters + const provisionenFilters = [ + 'filterAnbieterProvisionen', + 'filterTarifProvisionen', + 'filterSchaltungsIdProvisionen', + 'filterRufnummerProvisionen', + 'filterDatumVonProvisionen', + 'filterDatumBisProvisionen' + ]; + + provisionenFilters.forEach(filterId => { + const element = document.getElementById(filterId); + if (element) { + if (element.type === 'text') { + element.addEventListener('input', applyFiltersProvisionen); + } else { + element.addEventListener('change', applyFiltersProvisionen); + } + } + }); +}); + +// Tarifoptionen Administration Functions +async function loadTarifoptionenAdmin() { + await loadAnbieterForTarifoptionen(); + await displayTarifoptionenAdmin(); +} + +async function loadAnbieterForTarifoptionen() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const select = document.getElementById('tarifoptionAnbieter'); + select.innerHTML = ''; + anbieter.forEach(a => { + select.innerHTML += ``; + }); + } catch (error) { + console.error('Fehler beim Laden der Anbieter:', error); + } +} + +async function loadTarifeForOptions() { + const anbieterId = document.getElementById('tarifoptionAnbieter').value; + const container = document.getElementById('tarifCheckboxContainer'); + + if (!anbieterId) { + container.innerHTML = '
Erst Anbieter wählen...
'; + return; + } + + try { + const tarife = await apiRequest(`/api/tarife/${anbieterId}`); + + if (tarife.length === 0) { + container.innerHTML = '
Keine Tarife für diesen Anbieter gefunden.
'; + return; + } + + let checkboxHtml = ''; + tarife.forEach(tarif => { + checkboxHtml += ` +
+ +
+ `; + }); + + container.innerHTML = checkboxHtml; + } catch (error) { + console.error('Fehler beim Laden der Tarife:', error); + container.innerHTML = '
Fehler beim Laden der Tarife
'; + } +} + +// Toggle tariff selection based on anbieter-wide checkbox +function toggleTarifSelection() { + const checkbox = document.getElementById('tarifoptionAnbieterWeit'); + const tarifSelectionContainer = document.getElementById('tarifSelectionContainer'); + const anbieterSelect = document.getElementById('tarifoptionAnbieter'); + + if (checkbox.checked) { + // Anbieter-wide option - hide tarif selection + tarifSelectionContainer.style.display = 'none'; + } else { + // Tariff-specific option - show tarif selection + tarifSelectionContainer.style.display = 'block'; + if (anbieterSelect.value) { + loadTarifeForOptions(); + } else { + document.getElementById('tarifCheckboxContainer').innerHTML = + '
Erst Anbieter wählen...
'; + } + } +} + +// Select/Deselect all tarifs functions +function selectAllTarifs() { + const checkboxes = document.querySelectorAll('.tarif-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + }); +} + +function deselectAllTarifs() { + const checkboxes = document.querySelectorAll('.tarif-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + }); +} + +// Edit Tarifoption functions +function toggleEditTarifSelection() { + const checkbox = document.getElementById('editTarifoptionAnbieterWeit'); + const tarifSelectionContainer = document.getElementById('editTarifSelectionContainer'); + const anbieterSelect = document.getElementById('editTarifoptionAnbieter'); + if (checkbox.checked) { + // Anbieter-wide option - hide tarif selection + tarifSelectionContainer.style.display = 'none'; + } else { + // Tariff-specific option - show tarif selection + tarifSelectionContainer.style.display = 'block'; + if (anbieterSelect.value) { + loadTarifeForEditOptions(); + } else { + document.getElementById('editTarifCheckboxContainer').innerHTML = + '
Erst Anbieter wählen...
'; + } + } +} + +async function loadTarifeForEditOptions() { + const anbieterId = document.getElementById('editTarifoptionAnbieter').value; + const container = document.getElementById('editTarifCheckboxContainer'); + const counterElement = document.getElementById('editTarifCounter'); + + if (!anbieterId) { + container.innerHTML = '
Erst Anbieter wählen...
'; + counterElement.textContent = ''; + return; + } + + try { + const tarife = await apiRequest(`/api/tarife/${anbieterId}`); + if (tarife.length === 0) { + container.innerHTML = '
Keine Tarife für diesen Anbieter gefunden.
'; + counterElement.textContent = ''; + return; + } + + let checkboxHtml = '
'; + tarife.forEach((tarif, index) => { + const provisionText = tarif.provision_euro ? ` (${tarif.provision_euro}€)` : ''; + checkboxHtml += ` +
+ +
+ `; + }); + checkboxHtml += '
'; + container.innerHTML = checkboxHtml; + updateEditTarifCounter(); + + // Add hover effects + container.querySelectorAll('.edit-tarif-item').forEach(item => { + item.addEventListener('mouseenter', function() { + this.style.backgroundColor = '#e3f2fd'; + this.style.borderColor = '#2196f3'; + }); + item.addEventListener('mouseleave', function() { + this.style.backgroundColor = '#f8f9fa'; + this.style.borderColor = '#e9ecef'; + }); + }); + + // Add change listeners to checkboxes + container.querySelectorAll('.edit-tarif-checkbox').forEach(checkbox => { + checkbox.addEventListener('change', updateEditTarifCounter); + }); + + } catch (error) { + console.error('Fehler beim Laden der Tarife:', error); + container.innerHTML = '
Fehler beim Laden der Tarife.
'; + counterElement.textContent = ''; + } +} + +function selectAllEditTarifs() { + const checkboxes = document.querySelectorAll('.edit-tarif-checkbox:not([style*="display: none"])'); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + }); + updateEditTarifCounter(); +} + +function deselectAllEditTarifs() { + const checkboxes = document.querySelectorAll('.edit-tarif-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + }); + updateEditTarifCounter(); +} + +function updateEditTarifCounter() { + const totalCheckboxes = document.querySelectorAll('.edit-tarif-checkbox').length; + const checkedCheckboxes = document.querySelectorAll('.edit-tarif-checkbox:checked').length; + const visibleCheckboxes = document.querySelectorAll('.edit-tarif-checkbox:not([style*="display: none"])').length; + + const counterElement = document.getElementById('editTarifCounter'); + if (counterElement) { + if (totalCheckboxes === 0) { + counterElement.textContent = ''; + } else { + const filterActive = visibleCheckboxes < totalCheckboxes; + counterElement.textContent = `${checkedCheckboxes} von ${totalCheckboxes} Tarifen ausgewählt${filterActive ? ` (${visibleCheckboxes} sichtbar)` : ''}`; + } + } +} + +function filterEditTarifs() { + const searchTerm = document.getElementById('editTarifSearch').value.toLowerCase(); + const tarifItems = document.querySelectorAll('.edit-tarif-item'); + + tarifItems.forEach(item => { + const tarifName = item.getAttribute('data-tarif-name'); + if (tarifName && tarifName.includes(searchTerm)) { + item.style.display = 'block'; + } else { + item.style.display = 'none'; + } + }); + + updateEditTarifCounter(); +} + +// Add Tarifoption Modal Functions +function openAddTarifoptionModal() { + // Load anbieter for the dropdown + loadAnbieterForAddModal(); + + // Reset form + document.getElementById('addTarifoptionForm').reset(); + document.getElementById('addTarifoptionAnbieterWeit').checked = true; + toggleAddTarifSelection(); + + // Show modal + document.getElementById('addTarifoptionModal').style.display = 'block'; +} + +async function loadAnbieterForAddModal() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const anbieterSelect = document.getElementById('addTarifoptionAnbieter'); + anbieterSelect.innerHTML = ''; + anbieter.forEach(a => { + anbieterSelect.innerHTML += ``; + }); + } catch (error) { + console.error('Fehler beim Laden der Anbieter:', error); + } +} + +function toggleAddTarifSelection() { + const checkbox = document.getElementById('addTarifoptionAnbieterWeit'); + const tarifSelectionContainer = document.getElementById('addTarifSelectionContainer'); + const anbieterWeitInfo = document.getElementById('addAnbieterWeitInfo'); + const anbieterSelect = document.getElementById('addTarifoptionAnbieter'); + + if (checkbox.checked) { + // Anbieter-wide option - hide tarif selection + tarifSelectionContainer.style.display = 'none'; + anbieterWeitInfo.style.display = 'block'; + } else { + // Tariff-specific option - show tarif selection + tarifSelectionContainer.style.display = 'block'; + anbieterWeitInfo.style.display = 'none'; + if (anbieterSelect.value) { + loadTarifeForAddOptions(); + } else { + document.getElementById('addTarifCheckboxContainer').innerHTML = + '
Erst Anbieter wählen...
'; + } + } +} + +async function loadTarifeForAddOptions() { + const anbieterId = document.getElementById('addTarifoptionAnbieter').value; + const container = document.getElementById('addTarifCheckboxContainer'); + const counterElement = document.getElementById('addTarifCounter'); + + if (!anbieterId) { + container.innerHTML = '
Erst Anbieter wählen...
'; + counterElement.textContent = ''; + return; + } + + try { + const tarife = await apiRequest(`/api/tarife/${anbieterId}`); + if (tarife.length === 0) { + container.innerHTML = '
Keine Tarife für diesen Anbieter gefunden.
'; + counterElement.textContent = ''; + return; + } + + let checkboxHtml = '
'; + tarife.forEach((tarif, index) => { + checkboxHtml += ` +
+ +
+ `; + }); + checkboxHtml += '
'; + container.innerHTML = checkboxHtml; + updateAddTarifCounter(); + + // Add hover effects + container.querySelectorAll('.add-tarif-item').forEach(item => { + item.addEventListener('mouseenter', function() { + this.style.backgroundColor = '#e3f2fd'; + this.style.borderColor = '#2196f3'; + }); + item.addEventListener('mouseleave', function() { + this.style.backgroundColor = '#f8f9fa'; + this.style.borderColor = '#e9ecef'; + }); + }); + + // Add change listeners to checkboxes + container.querySelectorAll('.add-tarif-checkbox').forEach(checkbox => { + checkbox.addEventListener('change', updateAddTarifCounter); + }); + + } catch (error) { + console.error('Fehler beim Laden der Tarife:', error); + container.innerHTML = '
Fehler beim Laden der Tarife.
'; + counterElement.textContent = ''; + } +} + +function selectAllAddTarifs() { + const checkboxes = document.querySelectorAll('.add-tarif-checkbox:not([style*="display: none"])'); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + }); + updateAddTarifCounter(); +} + +function deselectAllAddTarifs() { + const checkboxes = document.querySelectorAll('.add-tarif-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + }); + updateAddTarifCounter(); +} + +function updateAddTarifCounter() { + const totalCheckboxes = document.querySelectorAll('.add-tarif-checkbox').length; + const checkedCheckboxes = document.querySelectorAll('.add-tarif-checkbox:checked').length; + const visibleCheckboxes = document.querySelectorAll('.add-tarif-checkbox:not([style*="display: none"])').length; + + const counterElement = document.getElementById('addTarifCounter'); + if (counterElement) { + if (totalCheckboxes === 0) { + counterElement.textContent = ''; + } else { + const filterActive = visibleCheckboxes < totalCheckboxes; + counterElement.textContent = `${checkedCheckboxes} von ${totalCheckboxes} Tarifen ausgewählt${filterActive ? ` (${visibleCheckboxes} sichtbar)` : ''}`; + } + } +} + +function filterAddTarifs() { + const searchTerm = document.getElementById('addTarifSearch').value.toLowerCase(); + const tarifItems = document.querySelectorAll('.add-tarif-item'); + + tarifItems.forEach(item => { + const tarifName = item.getAttribute('data-tarif-name'); + if (tarifName && tarifName.includes(searchTerm)) { + item.style.display = 'block'; + } else { + item.style.display = 'none'; + } + }); + + updateAddTarifCounter(); +} + +// Setup form submission for the new add modal +document.addEventListener('DOMContentLoaded', function() { + const addForm = document.getElementById('addTarifoptionForm'); + if (addForm) { + addForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + const anbieterId = document.getElementById('addTarifoptionAnbieter').value; + const isAnbieterWeit = document.getElementById('addTarifoptionAnbieterWeit').checked; + const name = document.getElementById('addTarifoptionName').value; + const code = document.getElementById('addTarifoptionCode').value; + const provisionEuro = document.getElementById('addTarifoptionProvisionBetrag').value; + const beschreibung = document.getElementById('addTarifoptionBeschreibung').value; + + if (!anbieterId || !name) { + showMessage('Bitte Anbieter und Name auswählen', 'error'); + return; + } + + let tarifIds = []; + if (!isAnbieterWeit) { + // Get selected tarifs from checkboxes + const checkedBoxes = document.querySelectorAll('.add-tarif-checkbox:checked'); + tarifIds = Array.from(checkedBoxes).map(cb => parseInt(cb.value)); + if (tarifIds.length === 0) { + showMessage('Bitte mindestens einen Tarif auswählen oder Anbieter-weit aktivieren', 'error'); + return; + } + } + + try { + const data = { + anbieter_id: anbieterId, + tarif_ids: tarifIds, + is_anbieter_weit: isAnbieterWeit, + name: name, + code: code || null, + provision_euro: parseFloat(provisionEuro) || 0, + beschreibung: beschreibung || null + }; + + const result = await apiRequest('/api/tarifoptionen/multi', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + showMessage(`Tarifoption "${name}" erfolgreich erstellt${result.assignments ? ` (${result.assignments} Tarif-Zuordnungen)` : ''}`, 'success'); + + // Close modal and refresh admin view + closeModal('addTarifoptionModal'); + if (typeof displayTarifoptionenAdmin === 'function') { + await displayTarifoptionenAdmin(); + } + + } catch (error) { + console.error('Fehler beim Erstellen der Tarifoption:', error); + // Error message is already shown by apiRequest function + } + }); + } +}); + +async function addTarifoption() { + const anbieterId = document.getElementById('tarifoptionAnbieter').value; + const isAnbieterWeit = document.getElementById('tarifoptionAnbieterWeit').checked; + const name = document.getElementById('tarifoptionName').value; + const code = document.getElementById('tarifoptionCode').value; + const provisionEuro = document.getElementById('tarifoptionProvisionBetrag').value; + const beschreibung = document.getElementById('tarifoptionBeschreibung').value; + + if (!anbieterId || !name) { + showMessage('Bitte Anbieter und Name auswählen', 'error'); + return; + } + + let tarifIds = []; + if (!isAnbieterWeit) { + // Get selected tarifs from checkboxes + const checkedBoxes = document.querySelectorAll('.tarif-checkbox:checked'); + tarifIds = Array.from(checkedBoxes).map(cb => parseInt(cb.value)); + + if (tarifIds.length === 0) { + showMessage('Bitte mindestens einen Tarif auswählen oder Anbieter-weit aktivieren', 'error'); + return; + } + } + + try { + const data = { + anbieter_id: anbieterId, + tarif_ids: tarifIds, + is_anbieter_weit: isAnbieterWeit, + name: name, + code: code || null, + provision_euro: parseFloat(provisionEuro) || 0, + beschreibung: beschreibung || null + }; + + const result = await apiRequest('/api/tarifoptionen/multi', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + showMessage(`Tarifoption "${name}" erfolgreich erstellt (${result.assignments} Tarif-Zuordnungen)`, 'success'); + + // Reset form + document.getElementById('tarifoptionAnbieter').value = ''; + document.getElementById('tarifoptionName').value = ''; + document.getElementById('tarifoptionCode').value = ''; + document.getElementById('tarifoptionProvisionBetrag').value = ''; + document.getElementById('tarifoptionBeschreibung').value = ''; + document.getElementById('tarifoptionAnbieterWeit').checked = true; + toggleTarifSelection(); + + // Reload tarifoptionen list + displayTarifoptionenAdmin(); + + } catch (error) { + console.error('Fehler beim Hinzufügen der Tarifoption:', error); + showMessage('Fehler beim Hinzufügen der Tarifoption: ' + error.message, 'error'); + } +} + +async function displayTarifoptionenAdmin() { + try { + // Try to get new format first, fallback to old format + let optionen; + try { + optionen = await apiRequest('/api/tarifoptionen/multi'); + } catch (error) { + console.log('New API not available, falling back to old format'); + optionen = await apiRequest('/api/tarifoptionen'); + } + + const tbody = document.getElementById('tarifoptionenAdminList'); + tbody.innerHTML = ''; + + if (optionen.length === 0) { + tbody.innerHTML = 'Keine Tarifoptionen vorhanden'; + return; + } + + optionen.forEach(option => { + const provision = `${(option.provision_euro || 0).toFixed(2).replace('.', ',')}€`; + + // Handle both old and new format + let tarifDisplay; + if (option.tarif_names) { + // New format with multiple tarifs + tarifDisplay = option.tarif_names || 'Alle Tarife'; + } else { + // Old format with single tarif + tarifDisplay = option.tarif_id ? + (option.tarif_name || 'Unbekannter Tarif') : + 'Alle Tarife'; + } + + // For new format, we may not have anbieter_name directly + const anbieterName = option.anbieter_name || 'Mehrere Anbieter'; + + const row = ` + + ${option.id} + ${anbieterName} + ${tarifDisplay} + ${option.name} + ${option.code || '-'} + ${provision} + ${option.beschreibung || '-'} + + + + + + `; + tbody.innerHTML += row; + }); + } catch (error) { + console.error('Fehler beim Laden der Tarifoptionen:', error); + const tbody = document.getElementById('tarifoptionenAdminList'); + tbody.innerHTML = 'Fehler beim Laden der Daten'; + } + + // Update search counter after loading data + updateTarifoptionSearchCounter(); +} + +// Tarifoptionen search functions +function filterTarifoptionenAdmin() { + const searchTerm = document.getElementById('tarifoptionSearch').value.toLowerCase(); + const tbody = document.getElementById('tarifoptionenAdminList'); + const rows = tbody.getElementsByTagName('tr'); + + let visibleCount = 0; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (row.cells.length < 2) continue; // Skip header or empty rows + + // Get text content from relevant columns (Anbieter, Tarif, Option, Code, Beschreibung) + const anbieterText = (row.cells[1]?.textContent || '').toLowerCase(); + const tarifText = (row.cells[2]?.textContent || '').toLowerCase(); + const optionText = (row.cells[3]?.textContent || '').toLowerCase(); + const codeText = (row.cells[4]?.textContent || '').toLowerCase(); + const beschreibungText = (row.cells[6]?.textContent || '').toLowerCase(); + + // Check if search term matches any of the relevant columns + const matches = anbieterText.includes(searchTerm) || + tarifText.includes(searchTerm) || + optionText.includes(searchTerm) || + codeText.includes(searchTerm) || + beschreibungText.includes(searchTerm); + + if (matches || searchTerm === '') { + row.style.display = ''; + visibleCount++; + } else { + row.style.display = 'none'; + } + } + + updateTarifoptionSearchCounter(); +} + +function clearTarifoptionSearch() { + document.getElementById('tarifoptionSearch').value = ''; + filterTarifoptionenAdmin(); +} + +function updateTarifoptionSearchCounter() { + const tbody = document.getElementById('tarifoptionenAdminList'); + const counterElement = document.getElementById('tarifoptionSearchCounter'); + const rows = tbody.getElementsByTagName('tr'); + + let totalCount = 0; + let visibleCount = 0; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (row.cells.length < 2) continue; // Skip header or empty rows + totalCount++; + if (row.style.display !== 'none') { + visibleCount++; + } + } + + if (counterElement) { + if (totalCount === 0) { + counterElement.textContent = ''; + } else { + const searchTerm = document.getElementById('tarifoptionSearch').value; + if (searchTerm && visibleCount < totalCount) { + counterElement.textContent = `${visibleCount} von ${totalCount} Optionen`; + } else { + counterElement.textContent = `${totalCount} Optionen`; + } + } + } +} + +async function editTarifoption(optionId) { + try { + // Load anbieter for the dropdown + const anbieter = await apiRequest('/api/anbieter'); + const anbieterSelect = document.getElementById('editTarifoptionAnbieter'); + anbieterSelect.innerHTML = ''; + anbieter.forEach(a => { + anbieterSelect.innerHTML += ``; + }); + + // Try to get from new API first, fallback to old API + let optionen; + let option; + + try { + optionen = await apiRequest('/api/tarifoptionen/multi'); + option = optionen.find(opt => opt.id == optionId); + } catch (error) { + console.log('New API not available, trying old API'); + optionen = await apiRequest('/api/tarifoptionen'); + option = optionen.find(opt => opt.id == optionId); + } + + if (!option) { + showMessage('Tarifoption nicht gefunden', 'error'); + return; + } + + // Fill modal with current data + document.getElementById('editTarifoptionId').value = option.id; + document.getElementById('editTarifoptionName').value = option.name || ''; + document.getElementById('editTarifoptionCode').value = option.code || ''; + document.getElementById('editTarifoptionProvisionBetrag').value = option.provision_euro || option.provision_betrag || ''; + document.getElementById('editTarifoptionBeschreibung').value = option.beschreibung || ''; + + // Parse tarif assignments and set anbieter + if (option.tarif_ids && option.tarif_ids.length > 0) { + // For multi-tarif options, we need to find the anbieter + const tarifIds = option.tarif_ids.split(',').map(id => parseInt(id.trim())); + if (tarifIds.length > 0) { + // Get the first tarif to determine anbieter + try { + const tarif = await apiRequest(`/api/tarif/${tarifIds[0]}`); + if (tarif && tarif.anbieter_id) { + anbieterSelect.value = tarif.anbieter_id; + + // Check if all tarifs belong to the same anbieter + let allTarifs = []; + for (let tarifId of tarifIds) { + try { + const t = await apiRequest(`/api/tarif/${tarifId}`); + if (t) allTarifs.push(t); + } catch (e) {} + } + + const allSameAnbieter = allTarifs.every(t => t.anbieter_id === tarif.anbieter_id); + const anbieterTarifs = await apiRequest(`/api/tarife/${tarif.anbieter_id}`); + const isAllTarifsOfAnbieter = anbieterTarifs.length === tarifIds.length && + anbieterTarifs.every(at => tarifIds.includes(at.id)); + + if (allSameAnbieter && isAllTarifsOfAnbieter) { + // This is an anbieter-wide option + document.getElementById('editTarifoptionAnbieterWeit').checked = true; + document.getElementById('editTarifSelectionContainer').style.display = 'none'; + } else { + // This is tarif-specific + document.getElementById('editTarifoptionAnbieterWeit').checked = false; + document.getElementById('editTarifSelectionContainer').style.display = 'block'; + + // Load and select specific tarifs + await loadTarifeForEditOptions(); + // Select the specific tarifs + setTimeout(() => { + tarifIds.forEach(tarifId => { + const checkbox = document.querySelector(`.edit-tarif-checkbox[value="${tarifId}"]`); + if (checkbox) checkbox.checked = true; + }); + }, 100); + } + } + } catch (error) { + console.error('Error loading tarif details:', error); + } + } + } else { + // Default to anbieter-wide if no specific tarifs + document.getElementById('editTarifoptionAnbieterWeit').checked = true; + document.getElementById('editTarifSelectionContainer').style.display = 'none'; + } + + // Show modal + document.getElementById('editTarifoptionModal').style.display = 'block'; + + // Setup form submission + document.getElementById('editTarifoptionForm').onsubmit = async (e) => { + e.preventDefault(); + + const anbieterId = document.getElementById('editTarifoptionAnbieter').value; + const isAnbieterWeit = document.getElementById('editTarifoptionAnbieterWeit').checked; + + if (!anbieterId) { + showMessage('Bitte wählen Sie einen Anbieter aus', 'error'); + return; + } + + let tarifIds = []; + if (!isAnbieterWeit) { + const checkedBoxes = document.querySelectorAll('.edit-tarif-checkbox:checked'); + tarifIds = Array.from(checkedBoxes).map(cb => parseInt(cb.value)); + if (tarifIds.length === 0) { + showMessage('Bitte wählen Sie mindestens einen Tarif aus oder aktivieren Sie "Anbieter-weit"', 'error'); + return; + } + } + + const formData = { + name: document.getElementById('editTarifoptionName').value.trim(), + code: document.getElementById('editTarifoptionCode').value.trim() || null, + provision_euro: document.getElementById('editTarifoptionProvisionBetrag').value ? + parseFloat(document.getElementById('editTarifoptionProvisionBetrag').value) : 0, + beschreibung: document.getElementById('editTarifoptionBeschreibung').value.trim() || null, + anbieter_id: parseInt(anbieterId), + is_anbieter_weit: isAnbieterWeit, + tarif_ids: tarifIds + }; + + try { + // Try new API first, fallback to old API + try { + await apiRequest(`/api/tarifoptionen/multi/${optionId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + } catch (error) { + console.log('New API not available, trying old API'); + await apiRequest(`/api/tarifoptionen/${optionId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: formData.name, + provision_euro: formData.provision_euro + }) + }); + } + + closeModal('editTarifoptionModal'); + showMessage('Tarifoption erfolgreich aktualisiert'); + await displayTarifoptionenAdmin(); + } catch (error) { + console.error('Fehler beim Aktualisieren der Tarifoption:', error); + // Error message is already shown by apiRequest function + } + }; + + } catch (error) { + console.error('Fehler beim Laden der Tarifoption-Daten:', error); + } +} + +async function deleteTarifoption(optionId) { + if (!confirm('Möchten Sie diese Tarifoption wirklich löschen? Dies entfernt auch alle Zuordnungen zu Tarifen.')) { + return; + } + + try { + // Try new API first, fallback to old API + let success = false; + try { + await apiRequest(`/api/tarifoptionen/multi/${optionId}`, { + method: 'DELETE' + }); + success = true; + } catch (error) { + console.log('New API not available, trying old API'); + await apiRequest(`/api/tarifoptionen/${optionId}`, { + method: 'DELETE' + }); + success = true; + } + + if (success) { + await displayTarifoptionenAdmin(); + showMessage('Tarifoption erfolgreich gelöscht', 'success'); + } + } catch (error) { + console.error('Fehler beim Löschen der Tarifoption:', error); + showMessage('Fehler beim Löschen der Tarifoption: ' + (error.message || 'Unbekannter Fehler'), 'error'); + } +} + +// Mitarbeiter-Prozentsätze Functions + +async function loadMitarbeiterProzentsaetzeAdmin() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const container = document.getElementById('mitarbeiterProzentsaetzeList'); + + container.innerHTML = ''; + + for (const a of anbieter) { + const prozentsaetze = await apiRequest(`/api/anbieter/${a.id}/mitarbeiter-prozentsaetze`); + + const providerDiv = document.createElement('div'); + providerDiv.className = 'admin-card'; + providerDiv.innerHTML = ` +
+

${a.name}

+
+
+
+
+ `; + container.appendChild(providerDiv); + + const prozentsaetzeList = document.getElementById(`prozentsaetze-${a.id}`); + + if (prozentsaetze.length === 0) { + prozentsaetzeList.innerHTML = '

Keine Mitarbeiter-Prozentsätze definiert

'; + } else { + prozentsaetze.forEach(p => { + const item = document.createElement('div'); + item.className = 'list-item'; + item.innerHTML = ` +
+ ${p.prozentsatz}% + ${p.beschreibung ? `- ${p.beschreibung}` : ''} +
Gültig ab: ${p.gueltig_ab} +
+
+ +
+ `; + prozentsaetzeList.appendChild(item); + }); + } + } + + } catch (error) { + console.error('Fehler beim Laden der Mitarbeiter-Prozentsätze:', error); + } +} + +async function loadMitarbeiterAnbieter() { + try { + const anbieter = await apiRequest('/api/anbieter'); + const select = document.getElementById('mitarbeiterAnbieter'); + + select.innerHTML = ''; + + anbieter.forEach(a => { + select.innerHTML += ``; + }); + + } catch (error) { + console.error('Fehler beim Laden der Anbieter:', error); + } +} + +async function addMitarbeiterProzentsatz() { + const anbieterId = document.getElementById('mitarbeiterAnbieter').value; + const prozentsatz = document.getElementById('mitarbeiterProzentsatz').value; + const gueltigAb = document.getElementById('mitarbeiterGueltigAb').value; + const beschreibung = document.getElementById('mitarbeiterBeschreibung').value.trim(); + + if (!anbieterId || !prozentsatz) { + showMessage('Bitte Anbieter und Prozentsatz auswählen', 'error'); + return; + } + + const prozentsatzNum = parseFloat(prozentsatz); + if (prozentsatzNum < 0 || prozentsatzNum > 100) { + showMessage('Prozentsatz muss zwischen 0 und 100 sein', 'error'); + return; + } + + try { + const data = { + anbieter_id: anbieterId, + prozentsatz: prozentsatzNum, + gueltig_ab: gueltigAb || new Date().toISOString().split('T')[0], + beschreibung: beschreibung || null + }; + + await apiRequest('/api/mitarbeiter-prozentsaetze', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + // Reset form + document.getElementById('mitarbeiterAnbieter').value = ''; + document.getElementById('mitarbeiterProzentsatz').value = ''; + document.getElementById('mitarbeiterGueltigAb').value = ''; + document.getElementById('mitarbeiterBeschreibung').value = ''; + + await loadMitarbeiterProzentsaetzeAdmin(); + showMessage('Mitarbeiter-Prozentsatz erfolgreich hinzugefügt'); + + } catch (error) { + console.error('Fehler beim Hinzufügen des Mitarbeiter-Prozentsatzes:', error); + showMessage('Fehler beim Hinzufügen des Mitarbeiter-Prozentsatzes', 'error'); + } +} + +async function deleteMitarbeiterProzentsatz(id) { + if (!confirm('Möchten Sie diesen Mitarbeiter-Prozentsatz wirklich löschen?')) { + return; + } + + try { + await apiRequest(`/api/mitarbeiter-prozentsaetze/${id}`, { + method: 'DELETE' + }); + + await loadMitarbeiterProzentsaetzeAdmin(); + showMessage('Mitarbeiter-Prozentsatz erfolgreich gelöscht'); + + } catch (error) { + console.error('Fehler beim Löschen des Mitarbeiter-Prozentsatzes:', error); + showMessage('Fehler beim Löschen des Mitarbeiter-Prozentsatzes', 'error'); + } +} + +// Tarif-Provisionen Functions +async function loadTarifProvisionsAdmin() { + try { + const tarife = await apiRequest('/api/tarife'); + const container = document.getElementById('tarifProvisionsList'); + + container.innerHTML = ''; + + for (const tarif of tarife) { + const tarifDiv = document.createElement('div'); + tarifDiv.className = 'admin-card'; + tarifDiv.innerHTML = ` +
+

${tarif.name} (${tarif.anbieter_name})

+ Basis-Provision: ${tarif.provision_euro}€ +
+
+
+
+ + + +
+ + Feste Mitarbeiter-Provision für diesen Tarif + +
+
+ `; + container.appendChild(tarifDiv); + } + + } catch (error) { + console.error('Fehler beim Laden der Tarif-Provisionen:', error); + } +} + +async function saveTarifProvision(tarifId) { + const provisionInput = document.getElementById(`provision-${tarifId}`); + const provision = parseFloat(provisionInput.value) || 0; + + try { + await apiRequest(`/api/tarife/${tarifId}/mitarbeiter-provision`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mitarbeiter_provision: provision }) + }); + + showMessage('Mitarbeiter-Provision erfolgreich gespeichert'); + + } catch (error) { + console.error('Fehler beim Speichern der Mitarbeiter-Provision:', error); + showMessage('Fehler beim Speichern der Mitarbeiter-Provision', 'error'); + } +} + +// PDF Viewer Functions +let currentPdf = null; +let currentPage = 1; +let totalPages = 0; +let currentScale = 1.0; +const scaleStep = 0.2; + +// Configure PDF.js worker +pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + +async function viewPdfDocument(documentId, filename) { + try { + // Show loading message + showMessage('PDF wird geladen...', 'success'); + + // Set PDF title + document.getElementById('pdfViewerTitle').textContent = `📋 ${filename}`; + + // Load PDF + const pdfUrl = `/api/dokumente/${documentId}/download`; + const loadingTask = pdfjsLib.getDocument(pdfUrl); + + currentPdf = await loadingTask.promise; + totalPages = currentPdf.numPages; + currentPage = 1; + currentScale = 1.0; + + // Update UI + document.getElementById('totalPages').textContent = totalPages; + document.getElementById('zoomLevel').textContent = '100%'; + + // Render first page + await renderPage(currentPage); + + // Show modal + document.getElementById('pdfViewerModal').classList.add('show'); + + // Update navigation buttons + updateNavigationButtons(); + + } catch (error) { + console.error('Fehler beim Laden des PDFs:', error); + showMessage('Fehler beim Laden des PDFs', 'error'); + } +} + +async function renderPage(pageNum) { + try { + const page = await currentPdf.getPage(pageNum); + const canvas = document.getElementById('pdfCanvas'); + const context = canvas.getContext('2d'); + + // Calculate viewport + const viewport = page.getViewport({ scale: currentScale }); + + // Set canvas dimensions + canvas.height = viewport.height; + canvas.width = viewport.width; + + // Render PDF page + const renderContext = { + canvasContext: context, + viewport: viewport + }; + + await page.render(renderContext).promise; + + // Update page number + document.getElementById('currentPage').textContent = currentPage; + + } catch (error) { + console.error('Fehler beim Rendern der Seite:', error); + showMessage('Fehler beim Rendern der Seite', 'error'); + } +} + +function updateNavigationButtons() { + const prevBtn = document.getElementById('prevPageBtn'); + const nextBtn = document.getElementById('nextPageBtn'); + + prevBtn.disabled = currentPage <= 1; + nextBtn.disabled = currentPage >= totalPages; +} + +async function prevPage() { + if (currentPage > 1) { + currentPage--; + await renderPage(currentPage); + updateNavigationButtons(); + } +} + +async function nextPage() { + if (currentPage < totalPages) { + currentPage++; + await renderPage(currentPage); + updateNavigationButtons(); + } +} + +async function zoomIn() { + currentScale += scaleStep; + await renderPage(currentPage); + updateZoomLevel(); +} + +async function zoomOut() { + if (currentScale > scaleStep) { + currentScale -= scaleStep; + await renderPage(currentPage); + updateZoomLevel(); + } +} + +function updateZoomLevel() { + const zoomPercent = Math.round(currentScale * 100); + document.getElementById('zoomLevel').textContent = `${zoomPercent}%`; +} + +function closePdfViewer() { + document.getElementById('pdfViewerModal').classList.remove('show'); + + // Cleanup + currentPdf = null; + currentPage = 1; + totalPages = 0; + currentScale = 1.0; + + // Clear canvas + const canvas = document.getElementById('pdfCanvas'); + const context = canvas.getContext('2d'); + context.clearRect(0, 0, canvas.width, canvas.height); +} + +// Close PDF viewer when clicking outside +document.addEventListener('click', function(event) { + const modal = document.getElementById('pdfViewerModal'); + if (event.target === modal) { + closePdfViewer(); + } +}); + +// Keyboard navigation for PDF viewer +document.addEventListener('keydown', function(event) { + const modal = document.getElementById('pdfViewerModal'); + if (modal.classList.contains('show')) { + switch(event.key) { + case 'ArrowLeft': + case 'ArrowUp': + event.preventDefault(); + prevPage(); + break; + case 'ArrowRight': + case 'ArrowDown': + event.preventDefault(); + nextPage(); + break; + case 'Escape': + event.preventDefault(); + closePdfViewer(); + break; + case '+': + case '=': + event.preventDefault(); + zoomIn(); + break; + case '-': + event.preventDefault(); + zoomOut(); + break; + } + } +}); + +// Hardware pricing fields toggle and validation +function toggleHardwarePricing() { + const hardwareInput = document.getElementById('hardware'); + const hardwarePricingFields = document.getElementById('hardwarePricingFields'); + const hardwareEkInput = document.getElementById('hardwareEk'); + const kundenverkaufspreisInput = document.getElementById('kundenverkaufspreis'); + + if (hardwareInput.value.trim() !== '') { + // Show pricing fields and make them required + hardwarePricingFields.style.display = 'block'; + hardwareEkInput.required = true; + kundenverkaufspreisInput.required = true; + } else { + // Hide pricing fields and remove required attribute + hardwarePricingFields.style.display = 'none'; + hardwareEkInput.required = false; + kundenverkaufspreisInput.required = false; + // Clear values + hardwareEkInput.value = ''; + kundenverkaufspreisInput.value = ''; + } +} + +// Add hardware pricing validation to form submission +function validateHardwarePricing() { + const hardwareInput = document.getElementById('hardware'); + const hardwareEkInput = document.getElementById('hardwareEk'); + const kundenverkaufspreisInput = document.getElementById('kundenverkaufspreis'); + + if (hardwareInput.value.trim() !== '') { + if (!hardwareEkInput.value || parseFloat(hardwareEkInput.value) < 0) { + showMessage('Hardware-EK ist erforderlich wenn Hardware angegeben wird', 'error'); + return false; + } + if (!kundenverkaufspreisInput.value || parseFloat(kundenverkaufspreisInput.value) < 0) { + showMessage('Kundenverkaufspreis ist erforderlich wenn Hardware angegeben wird', 'error'); + return false; + } + } + return true; +} + +// PDF Export Function for Provisionen +function exportProvisionenToPdf() { + try { + // Get current filter values + const anbieterFilter = document.getElementById('filterAnbieterProvisionen')?.value || ''; + const tarifFilter = document.getElementById('filterTarifProvisionen')?.value || ''; + const schaltungsIdFilter = document.getElementById('filterSchaltungsIdProvisionen')?.value || ''; + const rufnummerFilter = document.getElementById('filterRufnummerProvisionen')?.value || ''; + const datumVon = document.getElementById('filterDatumVonProvisionen')?.value || ''; + const datumBis = document.getElementById('filterDatumBisProvisionen')?.value || ''; + + // Get filtered data (same logic as applyFiltersProvisionen) + let filteredSchaltungen = []; + if (allProvisionen && allProvisionen.schaltungen) { + filteredSchaltungen = allProvisionen.schaltungen.filter(schaltung => { + // Anbieter filter + if (anbieterFilter && schaltung.anbieter_name !== anbieterFilter) { + return false; + } + + // Tarif filter + if (tarifFilter && schaltung.tarif_name !== tarifFilter) { + return false; + } + + // Schaltungs-ID filter (supports multiple comma-separated IDs) + if (schaltungsIdFilter) { + const schaltungsIds = schaltungsIdFilter.split(',').map(id => id.trim().toLowerCase()).filter(id => id.length > 0); + if (schaltungsIds.length > 0) { + const schaltungsIdLower = schaltung.schaltungs_id.toLowerCase(); + const matches = schaltungsIds.some(searchId => schaltungsIdLower.includes(searchId)); + if (!matches) { + return false; + } + } + } + + // Rufnummer filter (case insensitive partial match) + if (rufnummerFilter && !schaltung.rufnummer.toLowerCase().includes(rufnummerFilter.toLowerCase())) { + return false; + } + + // Date range filter + const schaltungsDatum = new Date(schaltung.schaltungs_datum); + if (datumVon && schaltungsDatum < new Date(datumVon)) { + return false; + } + if (datumBis && schaltungsDatum > new Date(datumBis)) { + return false; + } + + return true; + }); + } + + if (filteredSchaltungen.length === 0) { + showMessage('Keine Daten zum Exportieren gefunden', 'error'); + return; + } + + // Calculate totals + const totalMitarbeiterProvision = filteredSchaltungen.reduce((sum, row) => { + return sum + parseFloat(row.berechnete_mitarbeiter_provision || 0); + }, 0); + + const totalShopProvision = filteredSchaltungen.reduce((sum, row) => { + return sum + parseFloat(row.berechnete_shop_provision || 0); + }, 0); + + // Create PDF in landscape orientation + const { jsPDF } = window.jspdf; + const doc = new jsPDF('landscape'); + + // Add title + doc.setFontSize(18); + doc.setFont(undefined, 'bold'); + doc.text('Provisions-Übersicht', 20, 20); + + // Add filter info if any filters are applied + let yPos = 35; + doc.setFontSize(10); + doc.setFont(undefined, 'normal'); + + if (anbieterFilter || tarifFilter || datumVon || datumBis) { + doc.text('Aktive Filter:', 20, yPos); + yPos += 6; + + if (anbieterFilter) { + doc.text(`• Anbieter: ${anbieterFilter}`, 25, yPos); + yPos += 5; + } + if (tarifFilter) { + doc.text(`• Tarif: ${tarifFilter}`, 25, yPos); + yPos += 5; + } + if (datumVon) { + doc.text(`• Von: ${new Date(datumVon).toLocaleDateString('de-DE')}`, 25, yPos); + yPos += 5; + } + if (datumBis) { + doc.text(`• Bis: ${new Date(datumBis).toLocaleDateString('de-DE')}`, 25, yPos); + yPos += 5; + } + yPos += 5; + } + + // Add generation date + doc.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE')} um ${new Date().toLocaleTimeString('de-DE')}`, 20, yPos); + yPos += 15; + + // Table headers + const tableHeaders = [ + 'Schaltungs-ID', + 'Eingabe-Datum', + 'Schaltungsdatum', + 'Kunde', + 'Anbieter', + 'Tarif', + 'Hardware', + 'Hardware-EK', + 'Verkaufspreis', + 'Shop-Provision', + 'Mitarbeiter-Provision' + ]; + + // Set table properties for landscape orientation (11 columns now) + const columnWidths = [20, 20, 20, 35, 25, 30, 20, 20, 20, 25, 25]; // Adjusted for 11 columns + const startX = 10; + const rowHeight = 12; // Increased row height for text wrapping + let currentY = yPos; + + // Draw headers + doc.setFontSize(8); + doc.setFont(undefined, 'bold'); + + let currentX = startX; + tableHeaders.forEach((header, index) => { + doc.rect(currentX, currentY, columnWidths[index], rowHeight); + doc.text(header, currentX + 2, currentY + 8); + currentX += columnWidths[index]; + }); + currentY += rowHeight; + + // Draw data rows + doc.setFont(undefined, 'normal'); + + // Helper function to wrap text + const wrapText = (text, maxWidth) => { + if (!text) return ['']; + const words = text.toString().split(' '); + const lines = []; + let currentLine = ''; + + for (const word of words) { + const testLine = currentLine ? `${currentLine} ${word}` : word; + const testWidth = doc.getTextWidth(testLine); + + if (testWidth > maxWidth - 4) { // -4 for padding + if (currentLine) { + lines.push(currentLine); + currentLine = word; + } else { + lines.push(word); // Single word too long, force it + } + } else { + currentLine = testLine; + } + } + + if (currentLine) { + lines.push(currentLine); + } + + return lines.length > 0 ? lines : ['']; + }; + + filteredSchaltungen.forEach((schaltung, rowIndex) => { + // Check if we need a new page (landscape has more space) + if (currentY > 180) { + doc.addPage(); + currentY = 20; + + // Redraw headers on new page + doc.setFont(undefined, 'bold'); + currentX = startX; + tableHeaders.forEach((header, index) => { + doc.rect(currentX, currentY, columnWidths[index], rowHeight); + doc.text(header, currentX + 2, currentY + 8); + currentX += columnWidths[index]; + }); + currentY += rowHeight; + doc.setFont(undefined, 'normal'); + } + + // Prepare row data + const rawData = [ + schaltung.schaltungs_id || '', + schaltung.eingabe_datum ? new Date(schaltung.eingabe_datum).toLocaleDateString('de-DE') : '', + schaltung.schaltungs_datum ? new Date(schaltung.schaltungs_datum).toLocaleDateString('de-DE') : '', + `${schaltung.vorname || ''} ${schaltung.nachname || ''}`.trim() + (schaltung.firma ? ` (${schaltung.firma})` : ''), + schaltung.anbieter_name || '', + schaltung.tarif_name || '', + schaltung.hardware || '-', + schaltung.hardware_ek ? `${parseFloat(schaltung.hardware_ek).toFixed(2)} €` : '-', + schaltung.kundenverkaufspreis_netto ? `${parseFloat(schaltung.kundenverkaufspreis_netto).toFixed(2)} €` : '-', + `${schaltung.berechnete_shop_provision || '0.00'} €`, + `${schaltung.berechnete_mitarbeiter_provision || '0.00'} €` + ]; + + // Wrap text for each column + const wrappedData = rawData.map((data, index) => wrapText(data, columnWidths[index])); + + // Calculate actual row height needed + const maxLines = Math.max(...wrappedData.map(lines => lines.length)); + const actualRowHeight = Math.max(rowHeight, maxLines * 4 + 4); + + currentX = startX; + + // Draw row with proper text wrapping + wrappedData.forEach((lines, colIndex) => { + // Draw cell border + doc.rect(currentX, currentY, columnWidths[colIndex], actualRowHeight); + + // Draw text lines + lines.forEach((line, lineIndex) => { + const textY = currentY + 6 + (lineIndex * 4); + doc.text(line, currentX + 2, textY); + }); + + currentX += columnWidths[colIndex]; + }); + + currentY += actualRowHeight; + }); + + // Add totals + currentY += 10; + doc.setFont(undefined, 'bold'); + doc.setFontSize(10); + doc.text(`Gesamte Shop-Provision: ${totalShopProvision.toFixed(2)} €`, startX, currentY); + currentY += 8; + doc.text(`Gesamte Mitarbeiter-Provision: ${totalMitarbeiterProvision.toFixed(2)} €`, startX, currentY); + currentY += 8; + doc.text(`Gesamt: ${(totalShopProvision + totalMitarbeiterProvision).toFixed(2)} €`, startX, currentY); + + // Generate filename with current date and filters + let filename = 'Provisionen_'; + if (datumVon || datumBis) { + if (datumVon) filename += `von_${datumVon}_`; + if (datumBis) filename += `bis_${datumBis}_`; + } + filename += new Date().toISOString().split('T')[0] + '.pdf'; + + // Save the PDF + doc.save(filename); + + showMessage('PDF erfolgreich erstellt und heruntergeladen!', 'success'); + + } catch (error) { + console.error('Fehler beim PDF-Export:', error); + showMessage('Fehler beim Erstellen des PDFs', 'error'); + } +} + +// Add form validation for hardware pricing +document.addEventListener('DOMContentLoaded', function() { + const schaltungForm = document.getElementById('schaltungForm'); + if (schaltungForm) { + schaltungForm.addEventListener('submit', function(event) { + if (!validateHardwarePricing()) { + event.preventDefault(); + return false; + } + }); + } +}); diff --git a/server.js b/server.js new file mode 100644 index 0000000..ab63d43 --- /dev/null +++ b/server.js @@ -0,0 +1,2139 @@ +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}`); +}); \ No newline at end of file