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) <noreply@anthropic.com>
This commit is contained in:
1 parent
97ff779bcf
commit
041df61b24
5 files changed
+82
-16
No files matched your search
+4
-1
@@ -55,12 +55,15 @@ function initDatabase() {
|
|||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
|
|
||||||
db.serialize(() => {
|
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 (
|
db.run(`CREATE TABLE IF NOT EXISTS anbieter (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
default_vo_id INTEGER,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)`);
|
)`);
|
||||||
|
addColumnIfMissing(db, 'anbieter', 'default_vo_id INTEGER');
|
||||||
|
|
||||||
// Mitarbeiter-Prozentsätze pro Anbieter
|
// Mitarbeiter-Prozentsätze pro Anbieter
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS mitarbeiter_prozentsaetze (
|
db.run(`CREATE TABLE IF NOT EXISTS mitarbeiter_prozentsaetze (
|
||||||
|
|||||||
@@ -1273,6 +1273,7 @@
|
|||||||
|
|
||||||
<div class="admin-content-section">
|
<div class="admin-content-section">
|
||||||
<h4>Bestehende VOs</h4>
|
<h4>Bestehende VOs</h4>
|
||||||
|
<p style="color:#666; font-size:13px;">Die mit ★ markierte VO wird bei der Schaltungserfassung automatisch vorausgewählt.</p>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="admin-table">
|
<table class="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1281,6 +1282,7 @@
|
|||||||
<th>Nummer</th>
|
<th>Nummer</th>
|
||||||
<th>Bezeichnung</th>
|
<th>Bezeichnung</th>
|
||||||
<th>Aktiv</th>
|
<th>Aktiv</th>
|
||||||
|
<th>Standard</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+51
-14
@@ -68,7 +68,7 @@ async function renderWizardAnbieterButtons() {
|
|||||||
btn.className = 'btn-success';
|
btn.className = 'btn-success';
|
||||||
btn.style.cssText = 'padding:14px 22px; font-size:15px; min-width:140px;';
|
btn.style.cssText = 'padding:14px 22px; font-size:15px; min-width:140px;';
|
||||||
btn.textContent = a.name;
|
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);
|
container.appendChild(btn);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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('anbieter').value = anbieterId;
|
||||||
document.getElementById('wizardAnbieterLabel').textContent = '· ' + anbieterName;
|
document.getElementById('wizardAnbieterLabel').textContent = '· ' + anbieterName;
|
||||||
document.getElementById('wizardStep1').style.display = 'none';
|
document.getElementById('wizardStep1').style.display = 'none';
|
||||||
document.getElementById('wizardStep2').style.display = 'block';
|
document.getElementById('wizardStep2').style.display = 'block';
|
||||||
|
|
||||||
// Load VOs for this provider
|
|
||||||
const voSelect = document.getElementById('wizardVo');
|
const voSelect = document.getElementById('wizardVo');
|
||||||
voSelect.innerHTML = '<option value="">VO wählen...</option>';
|
voSelect.innerHTML = '<option value="">VO wählen...</option>';
|
||||||
|
document.getElementById('wizardTarif').innerHTML = '<option value="">Erst VO wählen...</option>';
|
||||||
|
document.getElementById('tarifoptionenSection').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`);
|
const vos = await apiRequest(`/api/vos?anbieter_id=${encodeURIComponent(anbieterId)}`);
|
||||||
vos.forEach(v => {
|
vos.forEach(v => {
|
||||||
@@ -93,12 +95,20 @@ async function wizardSelectAnbieter(anbieterId, anbieterName) {
|
|||||||
});
|
});
|
||||||
if (vos.length === 0) {
|
if (vos.length === 0) {
|
||||||
showMessage('Für diesen Anbieter wurden noch keine VOs angelegt. Bitte in der Administration → VOs eine VO anlegen.', 'error');
|
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 */ }
|
} catch (e) { /* error shown */ }
|
||||||
|
|
||||||
// Reset Tarif + Tarifoptionen
|
|
||||||
document.getElementById('wizardTarif').innerHTML = '<option value="">Erst VO wählen...</option>';
|
|
||||||
document.getElementById('tarifoptionenSection').style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wizardBackToStep1() {
|
function wizardBackToStep1() {
|
||||||
@@ -4067,22 +4077,34 @@ async function loadAnbieterForVoForm() {
|
|||||||
async function loadVosAdmin() {
|
async function loadVosAdmin() {
|
||||||
const tbody = document.getElementById('vosList');
|
const tbody = document.getElementById('vosList');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#666;">Lade...</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#666;">Lade...</td></tr>';
|
||||||
try {
|
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 = '';
|
tbody.innerHTML = '';
|
||||||
if (!vos.length) {
|
if (!vos.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#666;">Keine VOs vorhanden</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#666;">Keine VOs vorhanden</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vos.forEach(v => {
|
vos.forEach(v => {
|
||||||
|
const isDefault = defaultByAnbieter[v.anbieter_id] === v.id;
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${escapeHtml(v.anbieter_name)}</td>
|
<td>${escapeHtml(v.anbieter_name)}</td>
|
||||||
<td>${escapeHtml(v.nummer)}</td>
|
<td>${escapeHtml(v.nummer)}</td>
|
||||||
<td>${escapeHtml(v.bezeichnung || '')}</td>
|
<td>${escapeHtml(v.bezeichnung || '')}</td>
|
||||||
<td>${v.aktiv ? '<span style="color:#27ae60;">Ja</span>' : '<span style="color:#c0392b;">Nein</span>'}</td>
|
<td>${v.aktiv ? '<span style="color:#27ae60;">Ja</span>' : '<span style="color:#c0392b;">Nein</span>'}</td>
|
||||||
|
<td>${isDefault ? '<span title="Standard-VO" style="color:#f39c12; font-size:18px;">★</span>' : ''}</td>
|
||||||
<td>
|
<td>
|
||||||
|
${isDefault
|
||||||
|
? `<button data-vo-action="unset-default" data-anbieter-id="${v.anbieter_id}" style="margin-right:5px;">Standard entfernen</button>`
|
||||||
|
: `<button data-vo-action="set-default" data-anbieter-id="${v.anbieter_id}" data-id="${v.id}" style="margin-right:5px;">Als Standard</button>`}
|
||||||
<button data-vo-action="edit" data-id="${v.id}"
|
<button data-vo-action="edit" data-id="${v.id}"
|
||||||
data-anbieter-name="${escapeHtml(v.anbieter_name)}"
|
data-anbieter-name="${escapeHtml(v.anbieter_name)}"
|
||||||
data-nummer="${escapeHtml(v.nummer)}"
|
data-nummer="${escapeHtml(v.nummer)}"
|
||||||
@@ -4094,21 +4116,36 @@ async function loadVosAdmin() {
|
|||||||
tr.querySelectorAll('button[data-vo-action]').forEach(btn => {
|
tr.querySelectorAll('button[data-vo-action]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const action = btn.dataset.voAction;
|
const action = btn.dataset.voAction;
|
||||||
const id = parseInt(btn.dataset.id, 10);
|
|
||||||
if (action === 'edit') {
|
if (action === 'edit') {
|
||||||
editVo(id, btn.dataset.nummer, btn.dataset.bezeichnung, btn.dataset.aktiv === '1', btn.dataset.anbieterName);
|
editVo(parseInt(btn.dataset.id, 10), btn.dataset.nummer, btn.dataset.bezeichnung, btn.dataset.aktiv === '1', btn.dataset.anbieterName);
|
||||||
} else if (action === 'delete') {
|
} else if (action === 'delete') {
|
||||||
deleteVo(id, btn.dataset.label);
|
deleteVo(parseInt(btn.dataset.id, 10), btn.dataset.label);
|
||||||
|
} else if (action === 'set-default') {
|
||||||
|
setAnbieterDefaultVo(parseInt(btn.dataset.anbieterId, 10), parseInt(btn.dataset.id, 10));
|
||||||
|
} else if (action === 'unset-default') {
|
||||||
|
setAnbieterDefaultVo(parseInt(btn.dataset.anbieterId, 10), null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#c0392b;">Fehler beim Laden</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#c0392b;">Fehler beim Laden</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function addVo() {
|
||||||
const anbieter_id = document.getElementById('voAnbieter').value;
|
const anbieter_id = document.getElementById('voAnbieter').value;
|
||||||
const nummer = document.getElementById('voNummer').value.trim();
|
const nummer = document.getElementById('voNummer').value.trim();
|
||||||
|
|||||||
+23
-1
@@ -5,10 +5,32 @@ const asyncHandler = require('../middleware/asyncHandler');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', asyncHandler(async (req, res) => {
|
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);
|
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) => {
|
router.post('/', asyncHandler(async (req, res) => {
|
||||||
const { name } = req.body || {};
|
const { name } = req.body || {};
|
||||||
if (!name || !name.trim()) {
|
if (!name || !name.trim()) {
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ router.delete('/:id', asyncHandler(async (req, res) => {
|
|||||||
if (used.n_schaltungen > 0) parts.push(`${used.n_schaltungen} Schaltung(en)`);
|
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` });
|
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]);
|
const result = await db.run('DELETE FROM vos WHERE id = ?', [id]);
|
||||||
if (result.changes === 0) return res.status(404).json({ error: 'VO nicht gefunden' });
|
if (result.changes === 0) return res.status(404).json({ error: 'VO nicht gefunden' });
|
||||||
res.json({ message: 'VO erfolgreich gelöscht' });
|
res.json({ message: 'VO erfolgreich gelöscht' });
|
||||||
|
|||||||
Reference in new issue
Block a user