Speichern
Abbrechen
diff --git a/public/script.js b/public/script.js
index 48a3de4..b9c9d2a 100644
--- a/public/script.js
+++ b/public/script.js
@@ -34,18 +34,129 @@ document.addEventListener('DOMContentLoaded', async function() {
loadAnbieter();
loadSchaltungen();
setupFileUpload();
+ renderWizardAnbieterButtons();
// Set today's date as default
const today = new Date().toISOString().split('T')[0];
document.getElementById('eingabeDatum').value = today;
+ document.getElementById('schaltungsDatum').value = today;
+
+ // Erfasser autofill
+ const erfasser = document.getElementById('wizardErfasser');
+ if (erfasser) erfasser.value = currentUser.displayName || currentUser.username;
- // Setup form submission
document.getElementById('schaltungForm').addEventListener('submit', handleSchaltungSubmit);
- // Load all data in background for faster tab switching
preloadAllData();
});
+// --- Schaltung Wizard ---
+async function renderWizardAnbieterButtons() {
+ const container = document.getElementById('wizardAnbieterButtons');
+ if (!container) return;
+ container.innerHTML = '
Lade...
';
+ try {
+ const anbieter = await apiRequest('/api/anbieter');
+ container.innerHTML = '';
+ if (!anbieter.length) {
+ container.innerHTML = '
Keine Anbieter vorhanden. Bitte zuerst unter Administration einen Anbieter anlegen.
';
+ return;
+ }
+ anbieter.forEach(a => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'btn-success';
+ btn.style.cssText = 'padding:14px 22px; font-size:15px; min-width:140px;';
+ btn.textContent = a.name;
+ btn.addEventListener('click', () => wizardSelectAnbieter(a.id, a.name));
+ container.appendChild(btn);
+ });
+ } catch (e) {
+ container.innerHTML = '
Fehler beim Laden der Anbieter
';
+ }
+}
+
+async function wizardSelectAnbieter(anbieterId, anbieterName) {
+ document.getElementById('anbieter').value = anbieterId;
+ document.getElementById('wizardAnbieterLabel').textContent = '· ' + anbieterName;
+ document.getElementById('wizardStep1').style.display = 'none';
+ document.getElementById('wizardStep2').style.display = 'block';
+
+ // Load VOs for this provider
+ const voSelect = document.getElementById('wizardVo');
+ voSelect.innerHTML = '
VO wählen... ';
+ try {
+ const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`);
+ vos.forEach(v => {
+ const label = `${v.nummer}${v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''}`;
+ voSelect.innerHTML += `
${escapeHtml(label)} `;
+ });
+ if (vos.length === 0) {
+ showMessage('Für diesen Anbieter wurden noch keine VOs angelegt. Bitte in der Administration → VOs eine VO anlegen.', 'error');
+ }
+ } catch (e) { /* error shown */ }
+
+ // Reset Tarif + Tarifoptionen
+ document.getElementById('wizardTarif').innerHTML = '
Erst VO wählen... ';
+ document.getElementById('tarifoptionenSection').style.display = 'none';
+}
+
+function wizardBackToStep1() {
+ document.getElementById('wizardStep2').style.display = 'none';
+ document.getElementById('wizardStep1').style.display = 'block';
+}
+
+async function loadTarifeForWizard() {
+ const voId = document.getElementById('wizardVo').value;
+ const tarifSelect = document.getElementById('wizardTarif');
+ tarifSelect.innerHTML = '
Tarif wählen... ';
+ document.getElementById('tarifoptionenSection').style.display = 'none';
+ if (!voId) return;
+ try {
+ const tarife = await apiRequest(`/api/tarife?vo_id=${encodeURIComponent(voId)}`);
+ tarife.forEach(t => {
+ tarifSelect.innerHTML += `
${escapeHtml(t.name)} `;
+ });
+ } catch (e) { /* error shown */ }
+}
+
+async function loadTarifoptionenForWizard() {
+ const tarifId = document.getElementById('wizardTarif').value;
+ const section = document.getElementById('tarifoptionenSection');
+ const list = document.getElementById('tarifoptionenList');
+ const noOptions = document.getElementById('noTarifoptionen');
+ list.innerHTML = '';
+ noOptions.style.display = 'none';
+ if (!tarifId) {
+ section.style.display = 'none';
+ return;
+ }
+ section.style.display = 'block';
+ try {
+ const options = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`);
+ if (!options || options.length === 0) {
+ noOptions.style.display = 'block';
+ return;
+ }
+ options.forEach(opt => {
+ const wrapper = document.createElement('label');
+ wrapper.style.cssText = 'display:flex; align-items:flex-start; gap:8px; padding:8px; border:1px solid #ddd; border-radius:5px; background:#f9f9f9; cursor:pointer;';
+ const cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.name = 'tarifoptionen';
+ cb.value = opt.id;
+ wrapper.appendChild(cb);
+ const text = document.createElement('div');
+ const price = (opt.provision_euro >= 0 ? '+' : '') + opt.provision_euro + '€';
+ text.innerHTML = `
${escapeHtml(opt.name)} (${escapeHtml(price)})
+ ${opt.code ? `
Code: ${escapeHtml(opt.code)}
` : ''}
+ ${opt.beschreibung ? `
${escapeHtml(opt.beschreibung)}
` : ''}`;
+ wrapper.appendChild(text);
+ list.appendChild(wrapper);
+ });
+ } catch (e) { /* error shown */ }
+}
+
function renderUserBadge() {
if (!currentUser) return;
// Show admin-only navigation elements (e.g. the Benutzer tab) once we know the role.
@@ -272,19 +383,18 @@ function showMessage(message, type = 'success') {
async function loadAnbieter() {
try {
const anbieter = await apiRequest('/api/anbieter');
-
- const anbieterSelect = document.getElementById('anbieter');
+
+ // Note: #anbieter is now a hidden input populated by the wizard;
+ // we keep the helper only for selects that still exist (admin form).
const tarifAnbieterSelect = document.getElementById('tarifAnbieter');
-
- // Clear existing options
- anbieterSelect.innerHTML = '
Bitte wählen... ';
- tarifAnbieterSelect.innerHTML = '
Anbieter wählen... ';
-
- anbieter.forEach(a => {
- anbieterSelect.innerHTML += `
${escapeHtml(a.name)} `;
- tarifAnbieterSelect.innerHTML += `
${escapeHtml(a.name)} `;
- });
-
+ if (tarifAnbieterSelect) {
+ tarifAnbieterSelect.innerHTML = '
Anbieter wählen... ';
+ anbieter.forEach(a => {
+ tarifAnbieterSelect.innerHTML += `
${escapeHtml(a.name)} `;
+ });
+ }
+ // Refresh wizard buttons in case anbieters changed
+ renderWizardAnbieterButtons();
} catch (error) {
console.error('Fehler beim Laden der Anbieter:', error);
}
@@ -381,55 +491,57 @@ function hideTarifoptionen() {
// 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
+
+ // Wizard fields not part of FormData (hidden input has it but vo is on a non-named select).
+ schaltungData.vo_id = document.getElementById('wizardVo').value || null;
+
const selectedTarifoptionen = Array.from(document.querySelectorAll('input[name="tarifoptionen"]:checked'))
.map(checkbox => parseInt(checkbox.value));
-
- // Validate hardware length if provided
+ if (selectedTarifoptionen.length) {
+ schaltungData.tarifoptionen = selectedTarifoptionen;
+ }
+
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)
+ 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
-
+ hideTarifoptionen();
+
const today = new Date().toISOString().split('T')[0];
document.getElementById('eingabeDatum').value = today;
-
- // Reload overview and provisions if needed
+ document.getElementById('schaltungsDatum').value = today;
+
+ // Erfasser wieder befüllen (reset löscht's nicht da readonly, aber safety)
+ const erfasser = document.getElementById('wizardErfasser');
+ if (erfasser) erfasser.value = currentUser.displayName || currentUser.username;
+
+ // Zurück zu Schritt 1 für die nächste Schaltung
+ wizardBackToStep1();
+
await refreshDataAfterSchaltungChange();
-
+
} catch (error) {
console.error('Fehler beim Speichern der Schaltung:', error);
}
@@ -824,6 +936,10 @@ function showAdminCategory(category) {
case 'anbieter':
loadAnbieterAdmin();
break;
+ case 'vos':
+ loadVosAdmin();
+ loadAnbieterForVoForm();
+ break;
case 'tarife':
loadTarifeAdmin();
loadAnbieterForTarife();
@@ -897,17 +1013,21 @@ async function loadTarifeAdmin() {
tbody.innerHTML = '';
if (tarife.length === 0) {
- tbody.innerHTML = '
Keine Tarife vorhanden ';
+ tbody.innerHTML = '
Keine Tarife vorhanden ';
return;
}
-
+
tarife.forEach(t => {
const provision = `${(t.provision_euro || 0).toFixed(2).replace('.', ',')}€`;
+ const voLabel = t.vo_nummer
+ ? `${t.vo_nummer}${t.vo_bezeichnung ? ' (' + t.vo_bezeichnung + ')' : ''}`
+ : '—';
const tr = document.createElement('tr');
tr.id = `tarif-item-${t.id}`;
tr.innerHTML = `
${t.id}
${escapeHtml(t.anbieter_name || 'Unbekannt')}
+
${escapeHtml(voLabel)}
${escapeHtml(t.name)}
${escapeHtml(provision)}
${escapeHtml(t.beschreibung || '-')}
@@ -930,7 +1050,7 @@ async function loadTarifeAdmin() {
} catch (error) {
console.error('Fehler beim Laden der Tarif-Administration:', error);
const tbody = document.getElementById('tarifeList');
- tbody.innerHTML = '
Fehler beim Laden der Daten ';
+ tbody.innerHTML = '
Fehler beim Laden der Daten ';
} finally {
isLoadingTarifeAdmin = false;
}
@@ -1013,54 +1133,69 @@ async function loadTarifoptionAnbieter() {
async function addTarif() {
const name = document.getElementById('newTarif').value.trim();
const anbieterId = document.getElementById('tarifAnbieter').value;
+ const voId = document.getElementById('tarifVo').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 (!voId) {
+ showMessage('Bitte VO auswählen', 'error');
+ return;
+ }
+
if (provisionEuro && parseFloat(provisionEuro) < 0) {
showMessage('Provision kann nicht negativ sein', 'error');
return;
}
-
+
try {
const requestData = {
name,
anbieter_id: anbieterId,
+ vo_id: voId,
provision_euro: provisionEuro ? parseFloat(provisionEuro) : 0,
- beschreibung: beschreibung || null
+ beschreibung: beschreibung || null,
};
-
+
await apiRequest('/api/tarife', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(requestData)
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(requestData),
});
-
- // Reset form
+
document.getElementById('newTarif').value = '';
document.getElementById('tarifAnbieter').value = '';
+ document.getElementById('tarifVo').innerHTML = '
VO wählen... ';
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');
}
}
+// Populates the VO dropdown in the add-tariff form based on the selected provider.
+async function loadVosForTarifForm() {
+ const anbieterId = document.getElementById('tarifAnbieter').value;
+ const select = document.getElementById('tarifVo');
+ select.innerHTML = '
VO wählen... ';
+ if (!anbieterId) return;
+ try {
+ const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`);
+ vos.forEach(v => {
+ const label = `${v.nummer}${v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''}`;
+ select.innerHTML += `
${escapeHtml(label)} `;
+ });
+ } catch (e) { /* error already shown */ }
+}
+
// Commission management functions
async function loadProvisionen() {
@@ -1412,46 +1547,74 @@ function closeModal(modalId) {
// 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;
- }
-
+ const tariff = await apiRequest(`/api/tarif/${id}`);
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
+
+ // Populate VO dropdown with the tariff's provider VOs and preselect.
+ const voSelect = document.getElementById('editTariffVo');
+ voSelect.innerHTML = '
VO wählen... ';
+ const vos = await apiRequest(`/api/vos?anbieter_id=${tariff.anbieter_id}`);
+ vos.forEach(v => {
+ const label = `${v.nummer}${v.bezeichnung ? ' (' + v.bezeichnung + ')' : ''}`;
+ const opt = document.createElement('option');
+ opt.value = v.id;
+ opt.textContent = label;
+ if (tariff.vo_id == v.id) opt.selected = true;
+ voSelect.appendChild(opt);
+ });
+
+ // Populate tarifoptionen checkbox grid (anbieter-wide options, with assigned flag)
+ const container = document.getElementById('editTariffOptionenContainer');
+ container.innerHTML = '';
+ const options = await apiRequest(`/api/tarife/${id}/tarifoptionen-multi`);
+ if (!options || options.length === 0) {
+ container.innerHTML = '
Keine Tarifoptionen für diesen Anbieter vorhanden ';
+ } else {
+ options.forEach(o => {
+ const wrapper = document.createElement('label');
+ wrapper.style.cssText = 'display:flex; align-items:center; gap:8px; padding:4px 0; cursor:pointer;';
+ const cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.value = o.id;
+ cb.checked = !!o.assigned;
+ cb.className = 'edit-tariff-option-checkbox';
+ wrapper.appendChild(cb);
+ const span = document.createElement('span');
+ const price = (o.provision_euro >= 0 ? '+' : '') + o.provision_euro + '€';
+ span.textContent = `${o.name}${o.code ? ' [' + o.code + ']' : ''} (${price})`;
+ wrapper.appendChild(span);
+ container.appendChild(wrapper);
+ });
+ }
+
document.getElementById('editTariffModal').style.display = 'block';
-
- // Setup form submission
+
document.getElementById('editTariffForm').onsubmit = async (e) => {
e.preventDefault();
-
+ const checked = Array.from(container.querySelectorAll('.edit-tariff-option-checkbox:checked'))
+ .map(cb => parseInt(cb.value, 10));
const formData = {
name: document.getElementById('editTariffName').value.trim(),
- provision_euro: document.getElementById('editTariffProvisionBetrag').value ? parseFloat(document.getElementById('editTariffProvisionBetrag').value) : 0
+ vo_id: document.getElementById('editTariffVo').value || null,
+ provision_euro: document.getElementById('editTariffProvisionBetrag').value
+ ? parseFloat(document.getElementById('editTariffProvisionBetrag').value)
+ : 0,
+ tarifoption_ids: checked,
};
-
+
try {
await apiRequest(`/api/tarife/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(formData)
+ body: JSON.stringify(formData),
});
-
closeModal('editTariffModal');
showMessage('Tarif erfolgreich aktualisiert');
await loadTarifeAdmin();
@@ -1460,7 +1623,6 @@ async function editTariff(id) {
console.error('Fehler beim Aktualisieren des Tarifs:', error);
}
};
-
} catch (error) {
console.error('Fehler beim Laden der Tarif-Daten:', error);
}
@@ -3885,3 +4047,104 @@ async function deleteUser(uuid, username) {
// apiRequest already shows the error message
}
}
+
+// ============================================================================
+// VO-Verwaltung (Vertriebspartner-Nummern). Backend: /api/vos
+// ============================================================================
+
+async function loadAnbieterForVoForm() {
+ try {
+ const anbieter = await apiRequest('/api/anbieter');
+ const select = document.getElementById('voAnbieter');
+ if (!select) return;
+ select.innerHTML = '
Anbieter wählen... ';
+ anbieter.forEach(a => {
+ select.innerHTML += `
${escapeHtml(a.name)} `;
+ });
+ } catch (e) { /* error already shown */ }
+}
+
+async function loadVosAdmin() {
+ const tbody = document.getElementById('vosList');
+ if (!tbody) return;
+ tbody.innerHTML = '
Lade... ';
+ try {
+ const vos = await apiRequest('/api/vos');
+ tbody.innerHTML = '';
+ if (!vos.length) {
+ tbody.innerHTML = '
Keine VOs vorhanden ';
+ return;
+ }
+ vos.forEach(v => {
+ const tr = document.createElement('tr');
+ tr.innerHTML = `
+
${escapeHtml(v.anbieter_name)}
+
${escapeHtml(v.nummer)}
+
${escapeHtml(v.bezeichnung || '')}
+
${v.aktiv ? 'Ja ' : 'Nein '}
+
+ Bearbeiten
+ Löschen
+
+ `;
+ tr.querySelectorAll('button[data-vo-action]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const action = btn.dataset.voAction;
+ const id = parseInt(btn.dataset.id, 10);
+ if (action === 'edit') editVo(id, btn.dataset.nummer, btn.dataset.bezeichnung, btn.dataset.aktiv === '1');
+ else if (action === 'delete') deleteVo(id, btn.dataset.label);
+ });
+ });
+ tbody.appendChild(tr);
+ });
+ } catch (e) {
+ tbody.innerHTML = '
Fehler beim Laden ';
+ }
+}
+
+async function addVo() {
+ const anbieter_id = document.getElementById('voAnbieter').value;
+ const nummer = document.getElementById('voNummer').value.trim();
+ const bezeichnung = document.getElementById('voBezeichnung').value.trim();
+ if (!anbieter_id || !nummer) {
+ showMessage('Anbieter und VO-Nummer sind erforderlich', 'error');
+ return;
+ }
+ try {
+ await apiRequest('/api/vos', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ anbieter_id, nummer, bezeichnung }),
+ });
+ document.getElementById('voAnbieter').value = '';
+ document.getElementById('voNummer').value = '';
+ document.getElementById('voBezeichnung').value = '';
+ showMessage('VO erfolgreich hinzugefügt');
+ loadVosAdmin();
+ } catch (e) { /* already shown */ }
+}
+
+async function editVo(id, nummer, bezeichnung, aktiv) {
+ const newNummer = prompt('VO-Nummer:', nummer);
+ if (newNummer === null) return;
+ const newBezeichnung = prompt('Bezeichnung (leer = keine):', bezeichnung || '');
+ if (newBezeichnung === null) return;
+ try {
+ await apiRequest(`/api/vos/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nummer: newNummer.trim(), bezeichnung: newBezeichnung.trim() || null }),
+ });
+ showMessage('VO aktualisiert');
+ loadVosAdmin();
+ } catch (e) { /* already shown */ }
+}
+
+async function deleteVo(id, label) {
+ if (!confirm(`VO "${label}" wirklich löschen?`)) return;
+ try {
+ await apiRequest(`/api/vos/${id}`, { method: 'DELETE' });
+ showMessage('VO gelöscht');
+ loadVosAdmin();
+ } catch (e) { /* already shown */ }
+}
diff --git a/routes/schaltungen.js b/routes/schaltungen.js
index 9b411bb..40672c9 100644
--- a/routes/schaltungen.js
+++ b/routes/schaltungen.js
@@ -18,13 +18,12 @@ function validateAmount(a) {
function validateSchaltungBody(body) {
const { schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname,
- rufnummer, anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto } = body || {};
+ anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto } = body || {};
if (!schaltungs_id || !schaltungs_id.trim()) return 'Schaltungs-ID ist erforderlich';
if (!validateDate(eingabe_datum)) return 'Ungültiges Eingabe-Datum (Format: YYYY-MM-DD)';
if (!validateDate(schaltungs_datum)) return 'Ungültiges Schaltungs-Datum (Format: YYYY-MM-DD)';
if (!vorname || !vorname.trim()) return 'Vorname ist erforderlich';
if (!nachname || !nachname.trim()) return 'Nachname ist erforderlich';
- if (!rufnummer || !rufnummer.trim()) return 'Rufnummer ist erforderlich';
if (!anbieter_id) return 'Anbieter ist erforderlich';
if (!tarif_id) return 'Tarif ist erforderlich';
if (!validateHardware(hardware)) return 'Hardware muss genau 15 Zeichen lang sein';
@@ -51,19 +50,32 @@ router.post('/', asyncHandler(async (req, res) => {
if (err) return res.status(400).json({ error: err });
const b = req.body;
+ // rufnummer may be empty - existing DBs may have a NOT NULL constraint, so
+ // we pass an empty string rather than NULL.
+ const rufnummerValue = b.rufnummer && String(b.rufnummer).trim() ? String(b.rufnummer).trim() : '';
+ const tarifoptionen = Array.isArray(b.tarifoptionen) ? b.tarifoptionen : [];
try {
- const result = await db.run(
- `INSERT INTO schaltungen
- (schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer,
- anbieter_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- b.schaltungs_id, b.eingabe_datum, b.schaltungs_datum, b.vorname, b.nachname,
- b.firma || null, b.rufnummer, b.anbieter_id, b.tarif_id, b.hardware || null,
- b.hardware_ek || null, b.kundenverkaufspreis_netto || null,
- ]
- );
- res.json({ id: result.lastID, message: 'Schaltung erfolgreich erstellt' });
+ const newId = await db.transaction(async () => {
+ const result = await db.run(
+ `INSERT INTO schaltungen
+ (schaltungs_id, eingabe_datum, schaltungs_datum, vorname, nachname, firma, rufnummer,
+ anbieter_id, vo_id, tarif_id, hardware, hardware_ek, kundenverkaufspreis_netto)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ [
+ b.schaltungs_id, b.eingabe_datum, b.schaltungs_datum, b.vorname, b.nachname,
+ b.firma || null, rufnummerValue, b.anbieter_id, b.vo_id || null, b.tarif_id, b.hardware || null,
+ b.hardware_ek || null, b.kundenverkaufspreis_netto || null,
+ ]
+ );
+ for (const tarifoptionId of tarifoptionen) {
+ await db.run(
+ 'INSERT OR IGNORE INTO schaltungen_tarifoptionen (schaltung_id, tarifoption_id) VALUES (?, ?)',
+ [result.lastID, tarifoptionId]
+ );
+ }
+ return result.lastID;
+ });
+ res.json({ id: newId, message: 'Schaltung erfolgreich erstellt' });
} catch (e) {
if (/UNIQUE/.test(e.message)) {
return res.status(400).json({ error: 'Schaltungs-ID existiert bereits' });
@@ -79,17 +91,18 @@ router.put('/:id', asyncHandler(async (req, res) => {
const b = req.body;
const tarifoptionen = Array.isArray(b.tarifoptionen) ? b.tarifoptionen : [];
+ const rufnummerValue = b.rufnummer && String(b.rufnummer).trim() ? String(b.rufnummer).trim() : '';
await db.transaction(async () => {
const upd = await db.run(
`UPDATE schaltungen
SET schaltungs_id = ?, eingabe_datum = ?, schaltungs_datum = ?, vorname = ?, nachname = ?,
- firma = ?, rufnummer = ?, anbieter_id = ?, tarif_id = ?, hardware = ?,
+ firma = ?, rufnummer = ?, anbieter_id = ?, vo_id = ?, tarif_id = ?, hardware = ?,
hardware_ek = ?, kundenverkaufspreis_netto = ?
WHERE id = ?`,
[
b.schaltungs_id, b.eingabe_datum, b.schaltungs_datum, b.vorname, b.nachname,
- b.firma || null, b.rufnummer, b.anbieter_id, b.tarif_id, b.hardware || null,
+ b.firma || null, rufnummerValue, b.anbieter_id, b.vo_id || null, b.tarif_id, b.hardware || null,
b.hardware_ek || null, b.kundenverkaufspreis_netto || null, id,
]
);
diff --git a/routes/tarife.js b/routes/tarife.js
index 07c63f1..7a639cb 100644
--- a/routes/tarife.js
+++ b/routes/tarife.js
@@ -9,23 +9,38 @@ function validateAmount(amount) {
}
router.get('/', asyncHandler(async (req, res) => {
+ const { vo_id } = req.query;
+ const params = [];
+ let where = '';
+ if (vo_id) {
+ where = 'WHERE t.vo_id = ?';
+ params.push(vo_id);
+ }
const rows = await db.all(
- `SELECT t.*, a.name AS anbieter_name
+ `SELECT t.*, a.name AS anbieter_name, v.nummer AS vo_nummer, v.bezeichnung AS vo_bezeichnung
FROM tarife t
JOIN anbieter a ON t.anbieter_id = a.id
- ORDER BY a.name, t.name`
+ LEFT JOIN vos v ON t.vo_id = v.id
+ ${where}
+ ORDER BY a.name, t.name`,
+ params
);
res.json(rows);
}));
router.get('/:anbieter_id', asyncHandler(async (req, res) => {
const { anbieter_id } = req.params;
- const rows = await db.all('SELECT * FROM tarife WHERE anbieter_id = ? ORDER BY name', [anbieter_id]);
+ const rows = await db.all(
+ `SELECT t.*, v.nummer AS vo_nummer, v.bezeichnung AS vo_bezeichnung
+ FROM tarife t LEFT JOIN vos v ON t.vo_id = v.id
+ WHERE t.anbieter_id = ? ORDER BY t.name`,
+ [anbieter_id]
+ );
res.json(rows);
}));
router.post('/', asyncHandler(async (req, res) => {
- const { name, anbieter_id, provision_euro, beschreibung } = req.body || {};
+ const { name, anbieter_id, vo_id, provision_euro, beschreibung, tarifoption_ids } = req.body || {};
if (!name || !name.trim()) {
return res.status(400).json({ error: 'Tarifname ist erforderlich' });
@@ -33,19 +48,40 @@ router.post('/', asyncHandler(async (req, res) => {
if (!anbieter_id) {
return res.status(400).json({ error: 'Anbieter ist erforderlich' });
}
+ if (!vo_id) {
+ return res.status(400).json({ error: 'VO ist erforderlich' });
+ }
+ // Make sure the VO actually belongs to the same provider.
+ const vo = await db.get('SELECT anbieter_id FROM vos WHERE id = ?', [vo_id]);
+ if (!vo) return res.status(400).json({ error: 'VO nicht gefunden' });
+ if (vo.anbieter_id !== Number(anbieter_id)) {
+ return res.status(400).json({ error: 'VO gehört nicht zum gewählten Anbieter' });
+ }
if (!validateAmount(provision_euro)) {
return res.status(400).json({ error: 'Provision muss eine positive Zahl sein' });
}
try {
- const result = await db.run(
- `INSERT INTO tarife (name, anbieter_id, provision_euro, beschreibung) VALUES (?, ?, ?, ?)`,
- [name.trim(), anbieter_id, provision_euro || 0, beschreibung || null]
- );
+ const tarifId = await db.transaction(async () => {
+ const result = await db.run(
+ `INSERT INTO tarife (name, anbieter_id, vo_id, provision_euro, beschreibung) VALUES (?, ?, ?, ?, ?)`,
+ [name.trim(), anbieter_id, vo_id, provision_euro || 0, beschreibung || null]
+ );
+ if (Array.isArray(tarifoption_ids)) {
+ for (const optId of tarifoption_ids) {
+ await db.run(
+ 'INSERT OR IGNORE INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)',
+ [optId, result.lastID]
+ );
+ }
+ }
+ return result.lastID;
+ });
res.json({
- id: result.lastID,
+ id: tarifId,
name: name.trim(),
anbieter_id,
+ vo_id,
provision_euro: provision_euro || 0,
beschreibung: beschreibung || null,
});
@@ -59,19 +95,65 @@ router.post('/', asyncHandler(async (req, res) => {
router.put('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
- const { name, provision_euro, beschreibung } = req.body || {};
+ const { name, vo_id, provision_euro, beschreibung, tarifoption_ids } = req.body || {};
if (!name) return res.status(400).json({ error: 'Tarif-Name ist erforderlich' });
if (!validateAmount(provision_euro)) {
return res.status(400).json({ error: 'Provision muss eine positive Zahl sein' });
}
- const result = await db.run(
- `UPDATE tarife SET name = ?, provision_euro = ?, beschreibung = ? WHERE id = ?`,
- [name, provision_euro || 0, beschreibung || null, id]
- );
- if (result.changes === 0) return res.status(404).json({ error: 'Tarif nicht gefunden' });
+ const current = await db.get('SELECT anbieter_id FROM tarife WHERE id = ?', [id]);
+ if (!current) return res.status(404).json({ error: 'Tarif nicht gefunden' });
+
+ if (vo_id) {
+ const vo = await db.get('SELECT anbieter_id FROM vos WHERE id = ?', [vo_id]);
+ if (!vo) return res.status(400).json({ error: 'VO nicht gefunden' });
+ if (vo.anbieter_id !== current.anbieter_id) {
+ return res.status(400).json({ error: 'VO gehört nicht zum Anbieter des Tarifs' });
+ }
+ }
+
+ await db.transaction(async () => {
+ await db.run(
+ `UPDATE tarife SET name = ?, vo_id = ?, provision_euro = ?, beschreibung = ? WHERE id = ?`,
+ [name, vo_id || null, provision_euro || 0, beschreibung || null, id]
+ );
+ if (Array.isArray(tarifoption_ids)) {
+ await db.run('DELETE FROM tarifoptionen_tarife WHERE tarif_id = ?', [id]);
+ for (const optId of tarifoption_ids) {
+ await db.run(
+ 'INSERT OR IGNORE INTO tarifoptionen_tarife (tarifoption_id, tarif_id) VALUES (?, ?)',
+ [optId, id]
+ );
+ }
+ }
+ });
res.json({ message: 'Tarif erfolgreich aktualisiert' });
}));
+// All tarifoptionen of this tariff's provider, each annotated with whether it
+// is currently assigned to this tariff. Frontend uses this to render the
+// checkbox grid in the tariff editor.
+router.get('/:id/tarifoptionen-multi', asyncHandler(async (req, res) => {
+ const { id } = req.params;
+ const tarif = await db.get('SELECT anbieter_id FROM tarife WHERE id = ?', [id]);
+ if (!tarif) return res.status(404).json({ error: 'Tarif nicht gefunden' });
+ const rows = await db.all(
+ `SELECT tn.id, tn.name, tn.code, tn.provision_euro, tn.beschreibung,
+ CASE WHEN tt.tarifoption_id IS NULL THEN 0 ELSE 1 END AS assigned
+ FROM tarifoptionen_new tn
+ LEFT JOIN tarifoptionen_tarife tt
+ ON tt.tarifoption_id = tn.id AND tt.tarif_id = ?
+ WHERE tn.aktiv = 1
+ AND EXISTS (
+ SELECT 1 FROM tarifoptionen_tarife tt2
+ JOIN tarife t2 ON tt2.tarif_id = t2.id
+ WHERE tt2.tarifoption_id = tn.id AND t2.anbieter_id = ?
+ )
+ ORDER BY tn.name`,
+ [id, tarif.anbieter_id]
+ );
+ res.json(rows);
+}));
+
router.delete('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const used = await db.get(
diff --git a/routes/vos.js b/routes/vos.js
new file mode 100644
index 0000000..507e35c
--- /dev/null
+++ b/routes/vos.js
@@ -0,0 +1,92 @@
+const express = require('express');
+const db = require('../db');
+const asyncHandler = require('../middleware/asyncHandler');
+
+const router = express.Router();
+
+// List all VOs (optionally filtered by anbieter via ?anbieter_id=)
+router.get('/', asyncHandler(async (req, res) => {
+ const { anbieter_id } = req.query;
+ if (anbieter_id) {
+ const rows = await db.all(
+ `SELECT v.*, a.name AS anbieter_name
+ FROM vos v JOIN anbieter a ON v.anbieter_id = a.id
+ WHERE v.anbieter_id = ? AND v.aktiv = 1
+ ORDER BY v.nummer`,
+ [anbieter_id]
+ );
+ return res.json(rows);
+ }
+ const rows = await db.all(
+ `SELECT v.*, a.name AS anbieter_name
+ FROM vos v JOIN anbieter a ON v.anbieter_id = a.id
+ ORDER BY a.name, v.nummer`
+ );
+ res.json(rows);
+}));
+
+router.post('/', asyncHandler(async (req, res) => {
+ const { anbieter_id, nummer, bezeichnung } = req.body || {};
+ if (!anbieter_id) return res.status(400).json({ error: 'Anbieter ist erforderlich' });
+ if (!nummer || !String(nummer).trim()) return res.status(400).json({ error: 'VO-Nummer ist erforderlich' });
+ try {
+ const result = await db.run(
+ 'INSERT INTO vos (anbieter_id, nummer, bezeichnung) VALUES (?, ?, ?)',
+ [anbieter_id, String(nummer).trim(), bezeichnung ? String(bezeichnung).trim() : null]
+ );
+ res.json({ id: result.lastID, anbieter_id, nummer: String(nummer).trim(), bezeichnung: bezeichnung || null });
+ } catch (err) {
+ if (/UNIQUE/.test(err.message)) {
+ return res.status(400).json({ error: 'VO mit dieser Nummer existiert bereits für diesen Anbieter' });
+ }
+ throw err;
+ }
+}));
+
+router.put('/:id', asyncHandler(async (req, res) => {
+ const { id } = req.params;
+ const { nummer, bezeichnung, aktiv } = req.body || {};
+ const current = await db.get('SELECT * FROM vos WHERE id = ?', [id]);
+ if (!current) return res.status(404).json({ error: 'VO nicht gefunden' });
+ const nextNummer = nummer !== undefined ? String(nummer).trim() : current.nummer;
+ if (!nextNummer) return res.status(400).json({ error: 'VO-Nummer darf nicht leer sein' });
+ try {
+ await db.run(
+ 'UPDATE vos SET nummer = ?, bezeichnung = ?, aktiv = ? WHERE id = ?',
+ [
+ nextNummer,
+ bezeichnung !== undefined ? (bezeichnung ? String(bezeichnung).trim() : null) : current.bezeichnung,
+ aktiv !== undefined ? (aktiv ? 1 : 0) : current.aktiv,
+ id,
+ ]
+ );
+ res.json({ message: 'VO erfolgreich aktualisiert' });
+ } catch (err) {
+ if (/UNIQUE/.test(err.message)) {
+ return res.status(400).json({ error: 'VO mit dieser Nummer existiert bereits für diesen Anbieter' });
+ }
+ throw err;
+ }
+}));
+
+router.delete('/:id', asyncHandler(async (req, res) => {
+ const { id } = req.params;
+ // Prevent deletion if any tariff or schaltung references it.
+ const used = await db.get(
+ `SELECT
+ (SELECT COUNT(*) FROM tarife WHERE vo_id = ?) AS n_tarife,
+ (SELECT COUNT(*) FROM schaltungen WHERE vo_id = ?) AS n_schaltungen`,
+ [id, id]
+ );
+ if (used.n_tarife + used.n_schaltungen > 0) {
+ const parts = [];
+ if (used.n_tarife > 0) parts.push(`${used.n_tarife} Tarif(e)`);
+ if (used.n_schaltungen > 0) parts.push(`${used.n_schaltungen} Schaltung(en)`);
+ return res.status(400).json({ error: `VO kann nicht gelöscht werden: ${parts.join(', ')} verwenden diese VO` });
+ }
+ const result = await db.run('DELETE FROM vos WHERE id = ?', [id]);
+ if (result.changes === 0) return res.status(404).json({ error: 'VO nicht gefunden' });
+ res.json({ message: 'VO erfolgreich gelöscht' });
+}));
+
+module.exports = router;
diff --git a/server.js b/server.js
index 1cc5bd6..62f9df9 100644
--- a/server.js
+++ b/server.js
@@ -130,6 +130,7 @@ const provisionenRouter = require('./routes/provisionen');
const provisionsregelnRouter = require('./routes/provisionsregeln');
const mitarbeiterRouter = require('./routes/mitarbeiter');
const usersRouter = require('./routes/users');
+const vosRouter = require('./routes/vos');
app.use('/api/anbieter', anbieterRouter);
app.use('/api/tarife', tarifeRouter);
@@ -137,6 +138,7 @@ app.use('/api/schaltungen', schaltungenRouter);
app.use('/api/tarifoptionen', tarifoptionenRouter);
app.use('/api/mitarbeiter-prozentsaetze', mitarbeiterRouter);
app.use('/api/users', usersRouter);
+app.use('/api/vos', vosRouter);
app.use('/api', dokumenteRouter); // /api/schaltungen/:id/dokumente, /api/dokumente/:id/*
app.use('/api', provisionenRouter); // /api/provisionen, /api/schaltungen/:id/provision, /api/geloeschte-schaltungen
app.use('/api', provisionsregelnRouter); // /api/provisionsregeln, /api/anbieter-provisionsregeln