From 041df61b2442a2d3b42250e5b2237585cfae5b03 Mon Sep 17 00:00:00 2001 From: Kerim Atilgan Date: Fri, 15 May 2026 16:46:03 +0200 Subject: [PATCH] Per-Anbieter default VO with auto-select in the schaltung wizard - New nullable anbieter.default_vo_id column (migration adds it to existing DBs); deleting a VO that some provider points at clears the reference automatically. - GET /api/anbieter now includes default_vo_id + default_vo_nummer + default_vo_bezeichnung so the wizard can preselect without an extra round-trip. - New PUT /api/anbieter/:id/default-vo endpoint. Validates the VO belongs to the given provider; accepts vo_id=null to clear. - VO admin table grew a "Standard" column and an "Als Standard" / "Standard entfernen" button on each row. - When the user picks a provider in step 1 of the wizard, the VO dropdown now preselects the default VO (falling back to the only VO if there is exactly one), and immediately loads the matching tariffs. Co-Authored-By: Claude Opus 4.7 (1M context) --- database.js | 5 +++- public/index.html | 2 ++ public/script.js | 65 ++++++++++++++++++++++++++++++++++++---------- routes/anbieter.js | 24 ++++++++++++++++- routes/vos.js | 2 ++ 5 files changed, 82 insertions(+), 16 deletions(-) diff --git a/database.js b/database.js index 4cf55a5..05f694d 100644 --- a/database.js +++ b/database.js @@ -55,12 +55,15 @@ function initDatabase() { const db = getDatabase(); db.serialize(() => { - // Anbieter (Providers) + // Anbieter (Providers). default_vo_id picks which of this provider's + // VOs is preselected in the schaltung wizard. db.run(`CREATE TABLE IF NOT EXISTS anbieter ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, + default_vo_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); + addColumnIfMissing(db, 'anbieter', 'default_vo_id INTEGER'); // Mitarbeiter-Prozentsätze pro Anbieter db.run(`CREATE TABLE IF NOT EXISTS mitarbeiter_prozentsaetze ( diff --git a/public/index.html b/public/index.html index 916377a..357bdb7 100644 --- a/public/index.html +++ b/public/index.html @@ -1273,6 +1273,7 @@

Bestehende VOs

+

Die mit ★ markierte VO wird bei der Schaltungserfassung automatisch vorausgewählt.

@@ -1281,6 +1282,7 @@ + diff --git a/public/script.js b/public/script.js index b78f8fd..98cbb1b 100644 --- a/public/script.js +++ b/public/script.js @@ -68,7 +68,7 @@ async function renderWizardAnbieterButtons() { 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)); + btn.addEventListener('click', () => wizardSelectAnbieter(a.id, a.name, a.default_vo_id)); container.appendChild(btn); }); } catch (e) { @@ -76,15 +76,17 @@ async function renderWizardAnbieterButtons() { } } -async function wizardSelectAnbieter(anbieterId, anbieterName) { +async function wizardSelectAnbieter(anbieterId, anbieterName, defaultVoId) { 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 = ''; + document.getElementById('wizardTarif').innerHTML = ''; + document.getElementById('tarifoptionenSection').style.display = 'none'; + try { const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`); vos.forEach(v => { @@ -93,12 +95,20 @@ async function wizardSelectAnbieter(anbieterId, anbieterName) { }); if (vos.length === 0) { showMessage('Für diesen Anbieter wurden noch keine VOs angelegt. Bitte in der Administration → VOs eine VO anlegen.', 'error'); + return; + } + // Preselect the provider's default VO if set, otherwise the first VO if it is the only one. + let preselect = null; + if (defaultVoId && vos.some(v => v.id === defaultVoId)) { + preselect = defaultVoId; + } else if (vos.length === 1) { + preselect = vos[0].id; + } + if (preselect) { + voSelect.value = String(preselect); + await loadTarifeForWizard(); } } catch (e) { /* error shown */ } - - // Reset Tarif + Tarifoptionen - document.getElementById('wizardTarif').innerHTML = ''; - document.getElementById('tarifoptionenSection').style.display = 'none'; } function wizardBackToStep1() { @@ -4067,22 +4077,34 @@ async function loadAnbieterForVoForm() { async function loadVosAdmin() { const tbody = document.getElementById('vosList'); if (!tbody) return; - tbody.innerHTML = ''; + tbody.innerHTML = ''; try { - const vos = await apiRequest('/api/vos'); + // Load anbieter to know each one's default_vo_id. + const [vos, anbieter] = await Promise.all([ + apiRequest('/api/vos'), + apiRequest('/api/anbieter'), + ]); + const defaultByAnbieter = {}; + anbieter.forEach(a => { defaultByAnbieter[a.id] = a.default_vo_id; }); + tbody.innerHTML = ''; if (!vos.length) { - tbody.innerHTML = ''; + tbody.innerHTML = ''; return; } vos.forEach(v => { + const isDefault = defaultByAnbieter[v.anbieter_id] === v.id; const tr = document.createElement('tr'); tr.innerHTML = ` + '; + tbody.innerHTML = ''; } } +async function setAnbieterDefaultVo(anbieterId, voId) { + try { + await apiRequest(`/api/anbieter/${anbieterId}/default-vo`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ vo_id: voId }), + }); + showMessage(voId ? 'Standard-VO gesetzt' : 'Standard-VO entfernt'); + loadVosAdmin(); + } catch (e) { /* error already shown */ } +} + async function addVo() { const anbieter_id = document.getElementById('voAnbieter').value; const nummer = document.getElementById('voNummer').value.trim(); diff --git a/routes/anbieter.js b/routes/anbieter.js index 760c845..9a6de10 100644 --- a/routes/anbieter.js +++ b/routes/anbieter.js @@ -5,10 +5,32 @@ const asyncHandler = require('../middleware/asyncHandler'); const router = express.Router(); router.get('/', asyncHandler(async (req, res) => { - const rows = await db.all('SELECT * FROM anbieter ORDER BY name'); + const rows = await db.all( + `SELECT a.*, v.nummer AS default_vo_nummer, v.bezeichnung AS default_vo_bezeichnung + FROM anbieter a + LEFT JOIN vos v ON a.default_vo_id = v.id + ORDER BY a.name` + ); res.json(rows); })); +// Set or clear the default VO for a provider. Pass vo_id=null to clear. +router.put('/:id/default-vo', asyncHandler(async (req, res) => { + const { id } = req.params; + const { vo_id } = req.body || {}; + const anbieter = await db.get('SELECT id FROM anbieter WHERE id = ?', [id]); + if (!anbieter) return res.status(404).json({ error: 'Anbieter 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 !== Number(id)) { + return res.status(400).json({ error: 'VO gehört nicht zu diesem Anbieter' }); + } + } + await db.run('UPDATE anbieter SET default_vo_id = ? WHERE id = ?', [vo_id || null, id]); + res.json({ message: 'Standard-VO aktualisiert', default_vo_id: vo_id || null }); +})); + router.post('/', asyncHandler(async (req, res) => { const { name } = req.body || {}; if (!name || !name.trim()) { diff --git a/routes/vos.js b/routes/vos.js index 507e35c..4e8c680 100644 --- a/routes/vos.js +++ b/routes/vos.js @@ -84,6 +84,8 @@ router.delete('/:id', asyncHandler(async (req, res) => { 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` }); } + // Clear any anbieter pointing at this VO as their default before removing it. + await db.run('UPDATE anbieter SET default_vo_id = NULL WHERE default_vo_id = ?', [id]); 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' });
Nummer Bezeichnung AktivStandard Aktionen
Lade...
Lade...
Keine VOs vorhanden
Keine VOs vorhanden
${escapeHtml(v.anbieter_name)} ${escapeHtml(v.nummer)} ${escapeHtml(v.bezeichnung || '')} ${v.aktiv ? 'Ja' : 'Nein'}${isDefault ? '★' : ''} + ${isDefault + ? `` + : ``}
Fehler beim Laden
Fehler beim Laden