Add VO management, tariff-option assignment, and 2-step schaltung wizard

VOs (Vertriebspartner-Nummern)
- New vos table (one row per VO, scoped to a provider). Migrations
  add tarife.vo_id and schaltungen.vo_id columns (nullable so legacy
  rows survive).
- New /api/vos CRUD (list, create, update, delete with use-checks).
- Admin: new "VOs (Vertriebspartner)" sub-tab. List/create with
  provider dropdown; edit via prompt; delete refuses while used.
- /api/tarife?vo_id=X filter for fetching tariffs of a specific VO.

Tariff editor
- Add-tariff form requires a VO; new dependent VO dropdown that
  populates after the provider is chosen.
- Edit-tariff modal now shows the VO dropdown and a checkbox grid of
  all anbieter-wide tarifoptionen. Saving the dialog applies the
  selection via the existing tarifoptionen_tarife table.
- New GET /api/tarife/:id/tarifoptionen-multi returns the option set
  scoped to the tariff's provider, each annotated with whether it is
  currently assigned to this tariff.
- POST/PUT /api/tarife accept vo_id (required for create, validated
  against the provider) and an optional tarifoption_ids array.

Schaltung wizard
- The Schaltung-erfassen tab is now a 2-step wizard:
  step 1 picks a provider from a button row (mirrors the screenshot
  the user shared); step 2 shows a 2-column form mirroring the
  Aktivierungs-/Vertragsdaten layout.
- Step 2 cascades: VO -> tariffs filtered by VO -> tarifoptionen
  filtered by tariff. Erfasser is autofilled from the logged-in user
  (read-only).
- Removed customer fields that the user does not need (Kundennummer,
  Kennwort/Service-PIN never existed; Rufnummer is now optional both
  in the form and in the API validation).
- After saving, the wizard resets to step 1 ready for the next entry.

Schema
- tarife: vo_id added.
- schaltungen: vo_id added; rufnummer is nullable in new DBs.
  Existing DBs may still have NOT NULL on rufnummer; the schaltungen
  route now sends an empty string instead of NULL to remain
  compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kerimatilganandClaude Opus 4.7 committed 2026-05-15 16:36:22 +02:00
1 parent ea3a3fc12c
commit f2ab23f261
7 files changed
+768 -212

No files matched your search

+180 -96
View File
@@ -881,105 +881,130 @@
<button class="nav-tab admin-only" onclick="showTab('users')" style="display:none">Benutzer</button>
</div>
<!-- Schaltung erfassen Tab -->
<!-- Schaltung erfassen Tab (2-step wizard) -->
<div id="schaltung" class="tab-content active">
<h2>Neue Schaltung erfassen</h2>
<form id="schaltungForm">
<div class="form-row">
<div class="form-group">
<label for="schaltungsId">Schaltungs-ID <span class="required">*</span></label>
<input type="text" id="schaltungsId" name="schaltungs_id" required>
</div>
<div class="form-group">
<label for="eingabeDatum">Eingabe-Datum <span class="required">*</span></label>
<input type="date" id="eingabeDatum" name="eingabe_datum" required>
</div>
</div>
<div class="form-group">
<label for="schaltungsDatum">Schaltungs-/Aktivierungsdatum <span class="required">*</span></label>
<input type="date" id="schaltungsDatum" name="schaltungs_datum" required>
</div>
<div class="form-row-thirds">
<div class="form-group">
<label for="vorname">Vorname <span class="required">*</span></label>
<input type="text" id="vorname" name="vorname" required>
</div>
<div class="form-group">
<label for="nachname">Nachname <span class="required">*</span></label>
<input type="text" id="nachname" name="nachname" required>
</div>
<div class="form-group">
<label for="firma">Firma (optional)</label>
<input type="text" id="firma" name="firma">
</div>
</div>
<div class="form-group">
<label for="rufnummer">Rufnummer/Kundennummer <span class="required">*</span></label>
<input type="text" id="rufnummer" name="rufnummer" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="anbieter">Anbieter <span class="required">*</span></label>
<select id="anbieter" name="anbieter_id" required onchange="loadTarife()">
<option value="">Bitte wählen...</option>
</select>
</div>
<div class="form-group">
<label for="tarif">Tarif <span class="required">*</span></label>
<select id="tarif" name="tarif_id" required onchange="loadTarifoptionen()">
<option value="">Erst Anbieter wählen...</option>
</select>
</div>
</div>
<!-- Tarifoptionen/Aktionscodes -->
<div class="form-group" id="tarifoptionenSection" style="display: none;">
<label>Tarifoptionen/Aktionscodes (optional)</label>
<div id="tarifoptionenList" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px; margin-top: 10px;">
<!-- Optionen werden hier dynamisch eingefügt -->
</div>
<div id="noTarifoptionen" style="display: none; color: #666; font-style: italic; margin-top: 10px;">
Keine Tarifoptionen für diesen Tarif verfügbar
</div>
</div>
<div class="form-group">
<label for="hardware">Hardware (optional)</label>
<input type="text" id="hardware" name="hardware"
title="Hardware optional" onchange="toggleHardwarePricing()">
<!-- Step 1: Anbieter wählen -->
<div id="wizardStep1">
<h2>Neue Schaltung — Schritt 1 von 2</h2>
<p style="color:#666; margin-bottom:20px;">Wählen Sie einen Anbieter.</p>
<div id="wizardAnbieterButtons" style="display:flex; flex-wrap:wrap; gap:10px;"></div>
</div>
<!-- Step 2: VO + Tarif + Tarifoptionen + Kundendaten -->
<div id="wizardStep2" style="display:none;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;">
<h2 style="margin:0;">Neue Schaltung — Schritt 2 von 2 <small id="wizardAnbieterLabel" style="color:#3498db; font-weight:normal;"></small></h2>
<button type="button" onclick="wizardBackToStep1()" style="padding:6px 12px;">&larr; Anbieter ändern</button>
</div>
<!-- Hardware Pricing Fields -->
<div id="hardwarePricingFields" style="display: none;">
<div class="form-row">
<div class="form-group">
<label for="hardwareEk">Hardware-EK (Einkaufspreis) <span class="required">*</span></label>
<input type="number" id="hardwareEk" name="hardware_ek" step="0.01" min="0"
placeholder="0.00" title="Hardware-Einkaufspreis in Euro">
<form id="schaltungForm">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:30px;">
<!-- Linke Spalte: Aktivierungsdaten -->
<div>
<h3 style="margin-top:0; margin-bottom:15px;">Aktivierungsdaten</h3>
<div class="form-group">
<label for="wizardVo">VO-Auswahl <span class="required">*</span></label>
<select id="wizardVo" required onchange="loadTarifeForWizard()">
<option value="">VO wählen...</option>
</select>
</div>
<div class="form-group">
<label for="wizardTarif">Tarif <span class="required">*</span></label>
<select id="wizardTarif" name="tarif_id" required onchange="loadTarifoptionenForWizard()">
<option value="">Erst VO wählen...</option>
</select>
</div>
<div class="form-group" id="tarifoptionenSection" style="display:none;">
<label>Tarifoptionen</label>
<div id="tarifoptionenList" style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:8px; margin-top:8px;"></div>
<div id="noTarifoptionen" style="display:none; color:#666; font-style:italic; margin-top:8px;">Keine Tarifoptionen verfügbar</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="eingabeDatum">Erfassung <span class="required">*</span></label>
<input type="date" id="eingabeDatum" name="eingabe_datum" required>
</div>
<div class="form-group">
<label for="schaltungsDatum">Vertragsbeginn <span class="required">*</span></label>
<input type="date" id="schaltungsDatum" name="schaltungs_datum" required>
</div>
</div>
<div class="form-group">
<label>Erfasser</label>
<input type="text" id="wizardErfasser" readonly style="background:#f5f5f5;">
</div>
</div>
<div class="form-group">
<label for="kundenverkaufspreis">Kundenverkaufspreis (netto) <span class="required">*</span></label>
<input type="number" id="kundenverkaufspreis" name="kundenverkaufspreis_netto" step="0.01" min="0"
placeholder="0.00" title="Kundenverkaufspreis netto in Euro">
<!-- Rechte Spalte: Vertrags-/Kundendaten -->
<div>
<h3 style="margin-top:0; margin-bottom:15px;">Vertragsdaten</h3>
<div class="form-group">
<label for="schaltungsId">Schaltungs-ID <span class="required">*</span></label>
<input type="text" id="schaltungsId" name="schaltungs_id" required>
</div>
<div class="form-row-thirds">
<div class="form-group">
<label for="firma">Firma (optional)</label>
<input type="text" id="firma" name="firma">
</div>
<div class="form-group">
<label for="vorname">Vorname <span class="required">*</span></label>
<input type="text" id="vorname" name="vorname" required>
</div>
<div class="form-group">
<label for="nachname">Nachname <span class="required">*</span></label>
<input type="text" id="nachname" name="nachname" required>
</div>
</div>
<div class="form-group">
<label for="rufnummer">Rufnummer (optional)</label>
<input type="text" id="rufnummer" name="rufnummer">
</div>
<div class="form-group">
<label for="hardware">Hardware (optional, 15 Zeichen)</label>
<input type="text" id="hardware" name="hardware" onchange="toggleHardwarePricing()">
</div>
<div id="hardwarePricingFields" style="display:none;">
<div class="form-row">
<div class="form-group">
<label for="hardwareEk">Hardware-EK</label>
<input type="number" id="hardwareEk" name="hardware_ek" step="0.01" min="0" placeholder="0.00">
</div>
<div class="form-group">
<label for="kundenverkaufspreis">Kundenverkaufspreis</label>
<input type="number" id="kundenverkaufspreis" name="kundenverkaufspreis_netto" step="0.01" min="0" placeholder="0.00">
</div>
</div>
</div>
<div class="form-group">
<label>Dokumente hochladen</label>
<div class="file-upload-area" id="fileUploadArea">
<p>Dateien hier hineinziehen oder klicken zum Auswählen</p>
<input type="file" id="documentUpload" multiple accept=".pdf,.jpg,.jpeg,.png,.doc,.docx" style="display:none;">
</div>
<div id="uploadedFiles" class="documents-list"></div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Dokumente hochladen</label>
<div class="file-upload-area" id="fileUploadArea">
<p>Dateien hier hineinziehen oder klicken zum Auswählen</p>
<input type="file" id="documentUpload" multiple accept=".pdf,.jpg,.jpeg,.png,.doc,.docx" style="display: none;">
<input type="hidden" id="anbieter" name="anbieter_id">
<div style="margin-top:25px;">
<button type="submit" class="btn-success">Schaltung speichern</button>
</div>
<div id="uploadedFiles" class="documents-list"></div>
</div>
<button type="submit" class="btn-success">Schaltung speichern</button>
</form>
</form>
</div>
</div>
<!-- Übersicht Tab -->
@@ -1158,6 +1183,10 @@
<span class="admin-icon">🏢</span>
<span>Anbieter</span>
</button>
<button class="admin-cat-btn" onclick="showAdminCategory('vos')">
<span class="admin-icon">🔢</span>
<span>VOs (Vertriebspartner)</span>
</button>
<button class="admin-cat-btn" onclick="showAdminCategory('tarife')">
<span class="admin-icon">📋</span>
<span>Tarife</span>
@@ -1222,6 +1251,46 @@
</div>
</div>
<!-- VOs Management -->
<div id="admin-vos" class="admin-category">
<div class="admin-page">
<div class="admin-header">
<h3>VOs (Vertriebspartner-Nummern) verwalten</h3>
<p>Pro Anbieter können beliebig viele VOs angelegt werden. Jeder Tarif wird genau einer VO zugeordnet.</p>
</div>
<div class="admin-form-section">
<h4>Neue VO hinzufügen</h4>
<div class="add-form">
<select id="voAnbieter" style="margin-right:8px;">
<option value="">Anbieter wählen...</option>
</select>
<input type="text" id="voNummer" placeholder="VO-Nummer (z.B. 27518900)" style="margin-right:8px;">
<input type="text" id="voBezeichnung" placeholder="Bezeichnung (z.B. Fachhandel)" style="margin-right:8px;">
<button onclick="addVo()" class="btn-success">Hinzufügen</button>
</div>
</div>
<div class="admin-content-section">
<h4>Bestehende VOs</h4>
<div class="table-container">
<table class="admin-table">
<thead>
<tr>
<th>Anbieter</th>
<th>Nummer</th>
<th>Bezeichnung</th>
<th>Aktiv</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="vosList"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Tarife Management -->
<div id="admin-tarife" class="admin-category">
<div class="admin-page">
@@ -1234,9 +1303,12 @@
<h4>Neuen Tarif hinzufügen</h4>
<div class="tarif-form">
<div class="form-row">
<select id="tarifAnbieter">
<select id="tarifAnbieter" onchange="loadVosForTarifForm()">
<option value="">Anbieter wählen...</option>
</select>
<select id="tarifVo">
<option value="">VO wählen...</option>
</select>
<input type="text" id="newTarif" placeholder="Name des Tarifs">
</div>
<div class="form-row">
@@ -1259,6 +1331,7 @@
<tr>
<th>ID</th>
<th>Anbieter</th>
<th>VO</th>
<th>Tarif-Name</th>
<th>Provision (€)</th>
<th>Beschreibung</th>
@@ -1572,7 +1645,7 @@
<!-- Edit Tariff Modal -->
<div id="editTariffModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 30px; border-radius: 8px; width: 500px; max-width: 90%;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 30px; border-radius: 8px; width: 600px; max-width: 90%; max-height: 90vh; overflow-y: auto;">
<h3>Tarif bearbeiten</h3>
<form id="editTariffForm">
<input type="hidden" id="editTariffId">
@@ -1580,12 +1653,23 @@
<label>Tarif-Name:</label>
<input type="text" id="editTariffName" required>
</div>
<div class="form-group">
<label>VO:</label>
<select id="editTariffVo">
<option value="">VO wählen...</option>
</select>
</div>
<div class="form-group">
<label>Shop-Provision (€):</label>
<input type="number" id="editTariffProvisionBetrag" step="0.01" min="0" placeholder="Betrag in Euro">
</div>
<div class="form-group">
<label>Tarifoptionen (Anbieter-weit):</label>
<div id="editTariffOptionenContainer" style="border:1px solid #ddd; border-radius:6px; padding:10px; max-height:200px; overflow-y:auto; background:#fafafa;">
<small style="color:#666;">Keine Tarifoptionen für diesen Anbieter vorhanden</small>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="submit" class="btn-success">Speichern</button>
<button type="button" onclick="closeModal('editTariffModal')">Abbrechen</button>