Liittymien VLAN/Laite/IP-kentät hakevat nyt tiedot IPAM:sta ja laiterekisteristä

- NetAdmin liittymälomakkeen VLAN, Laite ja IP muutettu tekstikentistä dropdown-valikoiksi
- Asiakasformin liittymäkentät samoin muutettu dropdown-valikoiksi
- Dropdownit populoidaan IPAM:n VLANeista, IP-osoitteista ja Tekniikan laiterekisteristä
- IP-dropdown ryhmittelee vapaat ja varatut IP:t optgroupeilla
- Laite-dropdown näyttää ping-statuksen, hallintaosoitteen ja mallin
- VLAN-dropdown näyttää VLAN ID:n, nimen ja sijainnin
- Jos nykyinen arvo ei ole IPAM/laiterekisterissä, näytetään se (manuaalinen)-lisätekstillä
- IPAM-tilan automaattipäivitys: kun liittymälle asetetaan IP, IPAM merkitsee sen varatuksi
- Kun IP poistetaan tai vaihdetaan, vanha IP vapautetaan IPAM:ssa automaattisesti
- API palauttaa nyt vlans ja ips -listat netadmin_connections-endpointissa

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 00:20:42 +02:00
parent d9944922e9
commit 7ed17c163f
3 changed files with 206 additions and 13 deletions

43
api.php
View File

@@ -4412,6 +4412,11 @@ switch ($action) {
$deviceMap = []; $deviceMap = [];
foreach ($devices as $d) { $deviceMap[$d['nimi']] = $d; } foreach ($devices as $d) { $deviceMap[$d['nimi']] = $d; }
// Hae IPAM VLANit ja IP:t dropdown-valikkoja varten
$ipamAll = dbLoadIpam($companyId);
$vlans = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'vlan'));
$ips = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'ip'));
// Rikasta liittymädata laitetiedoilla // Rikasta liittymädata laitetiedoilla
foreach ($connections as &$conn) { foreach ($connections as &$conn) {
$deviceName = $conn['laite'] ?? ''; $deviceName = $conn['laite'] ?? '';
@@ -4423,7 +4428,9 @@ switch ($action) {
echo json_encode([ echo json_encode([
'connections' => $connections, 'connections' => $connections,
'total' => count($connections), 'total' => count($connections),
'devices' => $devices 'devices' => $devices,
'vlans' => $vlans,
'ips' => $ips
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
http_response_code(500); http_response_code(500);
@@ -4472,6 +4479,40 @@ switch ($action) {
echo json_encode(['error' => 'Liittymää ei löytynyt']); echo json_encode(['error' => 'Liittymää ei löytynyt']);
break; break;
} }
// Automaattinen IPAM-tilan päivitys kun IP muuttuu
$oldIp = trim($conn['ip'] ?? '');
$newIp = trim($input['ip'] ?? '');
if ($oldIp !== $newIp) {
$ipamAll = dbLoadIpam($companyId);
// Vapauta vanha IP
if ($oldIp) {
foreach ($ipamAll as $entry) {
if ($entry['tyyppi'] === 'ip' && $entry['verkko'] === $oldIp && $entry['tila'] === 'varattu') {
$entry['tila'] = 'vapaa';
$entry['asiakas'] = '';
$entry['muokattu'] = date('Y-m-d H:i:s');
$entry['muokkaaja'] = currentUser();
dbSaveIpam($companyId, $entry);
break;
}
}
}
// Varaa uusi IP
if ($newIp) {
$customerName = $conn['customer_name'] ?? '';
foreach ($ipamAll as $entry) {
if ($entry['tyyppi'] === 'ip' && $entry['verkko'] === $newIp) {
$entry['tila'] = 'varattu';
$entry['asiakas'] = $customerName;
$entry['muokattu'] = date('Y-m-d H:i:s');
$entry['muokkaaja'] = currentUser();
dbSaveIpam($companyId, $entry);
break;
}
}
}
}
dbUpdateConnection($connId, $input); dbUpdateConnection($connId, $input);
$updated = dbLoadConnection($connId); $updated = dbLoadConnection($connId);
echo json_encode($updated); echo json_encode($updated);

View File

@@ -1105,11 +1105,15 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>VLAN</label> <label>VLAN</label>
<input type="text" id="na-edit-vlan"> <select id="na-edit-vlan">
<option value="">- Ei VLANia -</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Laite</label> <label>Laite</label>
<input type="text" id="na-edit-laite"> <select id="na-edit-laite">
<option value="">- Ei laitetta -</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Portti</label> <label>Portti</label>
@@ -1117,7 +1121,9 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>IP</label> <label>IP</label>
<input type="text" id="na-edit-ip"> <select id="na-edit-ip">
<option value="">- Ei IP:tä -</option>
</select>
</div> </div>
</div> </div>
<div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;"> <div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;">

164
script.js
View File

@@ -684,15 +684,70 @@ function createLiittymaRow(data = {}, index = 0) {
<option value="36" ${data.sopimuskausi === '36' ? 'selected' : ''}>36 kk</option> <option value="36" ${data.sopimuskausi === '36' ? 'selected' : ''}>36 kk</option>
</select></div> </select></div>
<div class="form-group"><label>Alkaen</label><input type="date" class="l-alkupvm" value="${esc(data.alkupvm || '')}"></div> <div class="form-group"><label>Alkaen</label><input type="date" class="l-alkupvm" value="${esc(data.alkupvm || '')}"></div>
<div class="form-group"><label>VLAN</label><input type="text" class="l-vlan" value="${esc(data.vlan || '')}" placeholder="esim. 100"></div> <div class="form-group"><label>VLAN</label><select class="l-vlan"><option value="">- Ei VLANia -</option></select></div>
<div class="form-group"><label>Laite</label><input type="text" class="l-laite" value="${esc(data.laite || '')}" placeholder="esim. SW-CORE-01"></div> <div class="form-group"><label>Laite</label><select class="l-laite"><option value="">- Ei laitetta -</option></select></div>
<div class="form-group"><label>Portti</label><input type="text" class="l-portti" value="${esc(data.portti || '')}" placeholder="esim. Gi0/1"></div> <div class="form-group"><label>Portti</label><input type="text" class="l-portti" value="${esc(data.portti || '')}" placeholder="esim. Gi0/1"></div>
<div class="form-group"><label>IP</label><input type="text" class="l-ip" value="${esc(data.ip || '')}" placeholder="esim. 10.0.0.5"></div> <div class="form-group"><label>IP</label><select class="l-ip"><option value="">- Ei IP:tä -</option></select></div>
</div>`; </div>`;
div.querySelector('.btn-remove-row').addEventListener('click', () => { div.remove(); renumberLiittymaRows(); }); div.querySelector('.btn-remove-row').addEventListener('click', () => { div.remove(); renumberLiittymaRows(); });
// Populoi dropdownit IPAM/laite-datasta
populateLiittymaRowDropdowns(div, data);
return div; return div;
} }
// Populoi yksittäisen liittymärivin VLAN/Laite/IP dropdownit
function populateLiittymaRowDropdowns(row, data = {}) {
const vlanSel = row.querySelector('.l-vlan');
const laiteSel = row.querySelector('.l-laite');
const ipSel = row.querySelector('.l-ip');
// Käytä netadminData:a tai fallback ipamData/devicesData:an
const vlans = (netadminData.vlans && netadminData.vlans.length) ? netadminData.vlans : (ipamData || []).filter(e => e.tyyppi === 'vlan');
const ips = (netadminData.ips && netadminData.ips.length) ? netadminData.ips : (ipamData || []).filter(e => e.tyyppi === 'ip');
const devices = (netadminData.devices && netadminData.devices.length) ? netadminData.devices : (devicesData || []);
// VLAN
let vlanHtml = '<option value="">- Ei VLANia -</option>';
[...vlans].sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0)).forEach(v => {
const val = String(v.vlan_id || '');
const label = val + (v.nimi ? `${v.nimi}` : '');
vlanHtml += `<option value="${esc(val)}">${esc(label)}</option>`;
});
if (data.vlan && !vlans.some(v => String(v.vlan_id) === String(data.vlan))) {
vlanHtml += `<option value="${esc(data.vlan)}">${esc(data.vlan)} (manuaalinen)</option>`;
}
vlanSel.innerHTML = vlanHtml;
vlanSel.value = data.vlan || '';
// Laite
let laiteHtml = '<option value="">- Ei laitetta -</option>';
[...devices].sort((a, b) => (a.nimi || '').localeCompare(b.nimi || '')).forEach(d => {
const label = d.nimi + (d.hallintaosoite ? ` (${d.hallintaosoite})` : '');
laiteHtml += `<option value="${esc(d.nimi)}">${esc(label)}</option>`;
});
if (data.laite && !devices.some(d => d.nimi === data.laite)) {
laiteHtml += `<option value="${esc(data.laite)}">${esc(data.laite)} (manuaalinen)</option>`;
}
laiteSel.innerHTML = laiteHtml;
laiteSel.value = data.laite || '';
// IP
let ipHtml = '<option value="">- Ei IP:tä -</option>';
const free = ips.filter(i => i.tila === 'vapaa').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
const taken = ips.filter(i => i.tila === 'varattu').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
if (free.length) {
ipHtml += '<optgroup label="Vapaat IP:t">';
free.forEach(i => { ipHtml += `<option value="${esc(i.verkko)}">${esc(i.verkko)}${i.nimi ? `${esc(i.nimi)}` : ''}</option>`; });
ipHtml += '</optgroup>';
}
if (taken.length) {
ipHtml += '<optgroup label="Varatut IP:t">';
taken.forEach(i => { ipHtml += `<option value="${esc(i.verkko)}">${esc(i.verkko)}${i.asiakas ? `${esc(i.asiakas)}` : ''}</option>`; });
ipHtml += '</optgroup>';
}
if (data.ip && !ips.some(i => i.verkko === data.ip)) {
ipHtml += `<option value="${esc(data.ip)}">${esc(data.ip)} (manuaalinen)</option>`;
}
ipSel.innerHTML = ipHtml;
ipSel.value = data.ip || '';
}
function renumberLiittymaRows() { function renumberLiittymaRows() {
document.getElementById('liittymat-container').querySelectorAll('.liittyma-row').forEach((row, i) => { document.getElementById('liittymat-container').querySelectorAll('.liittyma-row').forEach((row, i) => {
row.dataset.index = i; row.dataset.index = i;
@@ -739,7 +794,7 @@ document.getElementById('btn-add').addEventListener('click', () => openCustomerF
document.getElementById('modal-close').addEventListener('click', () => customerModal.style.display = 'none'); document.getElementById('modal-close').addEventListener('click', () => customerModal.style.display = 'none');
document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none'); document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none');
function openCustomerForm(customer = null) { async function openCustomerForm(customer = null) {
const c = customer; const c = customer;
document.getElementById('modal-title').textContent = c ? 'Muokkaa asiakasta' : 'Lisää asiakas'; document.getElementById('modal-title').textContent = c ? 'Muokkaa asiakasta' : 'Lisää asiakas';
document.getElementById('form-submit').textContent = c ? 'Päivitä' : 'Tallenna'; document.getElementById('form-submit').textContent = c ? 'Päivitä' : 'Tallenna';
@@ -759,6 +814,8 @@ function openCustomerForm(customer = null) {
document.getElementById('form-priority-emails').value = c ? (c.priority_emails || '') : ''; document.getElementById('form-priority-emails').value = c ? (c.priority_emails || '') : '';
document.getElementById('form-billing-same').checked = false; document.getElementById('form-billing-same').checked = false;
document.getElementById('billing-fields').style.display = 'block'; document.getElementById('billing-fields').style.display = 'block';
// Lataa IPAM/laite-data dropdowendeja varten (jos ei vielä ladattu)
await ensureIpamDevicesLoaded();
const container = document.getElementById('liittymat-container'); const container = document.getElementById('liittymat-container');
container.innerHTML = ''; container.innerHTML = '';
(c ? (c.liittymat || []) : [{}]).forEach((l, i) => container.appendChild(createLiittymaRow(l, i))); (c ? (c.liittymat || []) : [{}]).forEach((l, i) => container.appendChild(createLiittymaRow(l, i)));
@@ -766,7 +823,27 @@ function openCustomerForm(customer = null) {
document.getElementById('form-yritys').focus(); document.getElementById('form-yritys').focus();
} }
function editCustomer(id) { const c = customers.find(x => x.id === id); if (c) openCustomerForm(c); } // Varmista IPAM/laite-data on ladattu dropdowneja varten
async function ensureIpamDevicesLoaded() {
try {
// Jos netadminData:ssa ei ole IPAM-dataa, lataa suoraan
if (!netadminData.vlans || !netadminData.vlans.length || !netadminData.devices || !netadminData.devices.length) {
const [ipam, devices] = await Promise.all([
apiCall('ipam'),
apiCall('devices')
]);
if (!netadminData.vlans || !netadminData.vlans.length) {
netadminData.vlans = ipam.filter(e => e.tyyppi === 'vlan');
netadminData.ips = ipam.filter(e => e.tyyppi === 'ip');
}
if (!netadminData.devices || !netadminData.devices.length) {
netadminData.devices = devices;
}
}
} catch (e) { console.error('IPAM/laite-datan lataus epäonnistui:', e); }
}
async function editCustomer(id) { const c = customers.find(x => x.id === id); if (c) await openCustomerForm(c); }
async function deleteCustomer(id, name) { async function deleteCustomer(id, name) {
if (!confirm(`Arkistoidaanko asiakas "${name}"?\n\nAsiakas siirretään arkistoon, josta sen voi palauttaa.`)) return; if (!confirm(`Arkistoidaanko asiakas "${name}"?\n\nAsiakas siirretään arkistoon, josta sen voi palauttaa.`)) return;
@@ -4518,7 +4595,7 @@ document.getElementById('btn-time-save')?.addEventListener('click', () => addTim
// ==================== NETADMIN ==================== // ==================== NETADMIN ====================
let netadminData = { connections: [], devices: [] }; let netadminData = { connections: [], devices: [], vlans: [], ips: [] };
async function loadNetadmin() { async function loadNetadmin() {
try { try {
@@ -4615,6 +4692,74 @@ document.getElementById('netadmin-filter-city')?.addEventListener('change', rend
document.getElementById('netadmin-filter-speed')?.addEventListener('change', renderNetadminTable); document.getElementById('netadmin-filter-speed')?.addEventListener('change', renderNetadminTable);
document.getElementById('netadmin-filter-device')?.addEventListener('change', renderNetadminTable); document.getElementById('netadmin-filter-device')?.addEventListener('change', renderNetadminTable);
// Populoi VLAN-dropdown IPAM VLANeista
function populateVlanDropdown(selectEl, currentValue) {
const vlans = netadminData.vlans || [];
let html = '<option value="">- Ei VLANia -</option>';
vlans.sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0));
vlans.forEach(v => {
const val = String(v.vlan_id || '');
const label = val + (v.nimi ? `${v.nimi}` : '') + (v.site_name ? ` (${v.site_name})` : '');
html += `<option value="${esc(val)}">${esc(label)}</option>`;
});
// Jos nykyinen arvo ei ole listalla, lisää se custom-optiona
if (currentValue && !vlans.some(v => String(v.vlan_id) === String(currentValue))) {
html += `<option value="${esc(currentValue)}">${esc(currentValue)} (manuaalinen)</option>`;
}
selectEl.innerHTML = html;
selectEl.value = currentValue || '';
}
// Populoi laite-dropdown
function populateDeviceDropdown(selectEl, currentValue) {
const devices = netadminData.devices || [];
let html = '<option value="">- Ei laitetta -</option>';
devices.sort((a, b) => (a.nimi || '').localeCompare(b.nimi || ''));
devices.forEach(d => {
const label = d.nimi + (d.hallintaosoite ? ` (${d.hallintaosoite})` : '') + (d.malli ? `${d.malli}` : '');
const pingDot = d.ping_status === 'up' ? '🟢 ' : d.ping_status === 'down' ? '🔴 ' : '';
html += `<option value="${esc(d.nimi)}">${pingDot}${esc(label)}</option>`;
});
// Jos nykyinen arvo ei ole listalla, lisää se custom-optiona
if (currentValue && !devices.some(d => d.nimi === currentValue)) {
html += `<option value="${esc(currentValue)}">${esc(currentValue)} (manuaalinen)</option>`;
}
selectEl.innerHTML = html;
selectEl.value = currentValue || '';
}
// Populoi IP-dropdown IPAM IP-osoitteista
function populateIpDropdown(selectEl, currentValue) {
const ips = netadminData.ips || [];
let html = '<option value="">- Ei IP:tä -</option>';
// Ryhmittele: vapaat ensin, sitten varatut
const free = ips.filter(i => i.tila === 'vapaa').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
const taken = ips.filter(i => i.tila === 'varattu').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
if (free.length) {
html += '<optgroup label="Vapaat IP:t">';
free.forEach(i => {
const label = i.verkko + (i.nimi ? `${i.nimi}` : '') + (i.site_name ? ` (${i.site_name})` : '');
html += `<option value="${esc(i.verkko)}">${esc(label)}</option>`;
});
html += '</optgroup>';
}
if (taken.length) {
html += '<optgroup label="Varatut IP:t">';
taken.forEach(i => {
const label = i.verkko + (i.asiakas ? `${i.asiakas}` : '') + (i.nimi ? `${i.nimi}` : '');
html += `<option value="${esc(i.verkko)}">${esc(label)}</option>`;
});
html += '</optgroup>';
}
// Jos nykyinen arvo ei ole listalla, lisää se custom-optiona
if (currentValue && !ips.some(i => i.verkko === currentValue)) {
html += `<option value="${esc(currentValue)}">${esc(currentValue)} (manuaalinen)</option>`;
}
selectEl.innerHTML = html;
selectEl.value = currentValue || '';
}
async function openNetadminDetail(connId) { async function openNetadminDetail(connId) {
try { try {
const conn = await apiCall(`netadmin_connection&id=${connId}`); const conn = await apiCall(`netadmin_connection&id=${connId}`);
@@ -4634,10 +4779,11 @@ async function openNetadminDetail(connId) {
speedSel.insertBefore(opt, speedSel.lastElementChild); speedSel.insertBefore(opt, speedSel.lastElementChild);
} }
speedSel.value = speed; speedSel.value = speed;
document.getElementById('na-edit-vlan').value = conn.vlan || ''; // Populoi VLAN, Laite ja IP dropdownit IPAM/Tekniikka-datasta
document.getElementById('na-edit-laite').value = conn.laite || ''; populateVlanDropdown(document.getElementById('na-edit-vlan'), conn.vlan || '');
populateDeviceDropdown(document.getElementById('na-edit-laite'), conn.laite || '');
populateIpDropdown(document.getElementById('na-edit-ip'), conn.ip || '');
document.getElementById('na-edit-portti').value = conn.portti || ''; document.getElementById('na-edit-portti').value = conn.portti || '';
document.getElementById('na-edit-ip').value = conn.ip || '';
document.getElementById('netadmin-detail-modal').style.display = ''; document.getElementById('netadmin-detail-modal').style.display = '';
} catch (e) { alert('Liittymän avaus epäonnistui: ' + e.message); } } catch (e) { alert('Liittymän avaus epäonnistui: ' + e.message); }
} }