From 70bd095b24ebba984790a1b06e78ead5a0247a9c Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Thu, 12 Mar 2026 11:09:52 +0200 Subject: [PATCH] =?UTF-8?q?NetAdmin:=20Gateway-kentt=C3=A4,=20IPAM-integra?= =?UTF-8?q?atio=20VLAN/IP-tietoihin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lisää Gateway-sarake ja -valitsin NetAdmin-näkymään (devices-linkitys) - VLAN ja IP näytetään IPAM:sta automaattisesti asiakkaan nimellä - Muokkausmodaalissa asiakkaan IPAM VLANit/IP:t näkyvät ensimmäisinä - DB: gateway_device_id LEFT JOIN devices, IPAM-enrichment API:ssa Co-Authored-By: Claude Opus 4.6 --- api.php | 39 ++++++++++++++++++++++++- db.php | 15 ++++++++-- index.html | 9 ++++++ script.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++++----- style.css | 10 +++++++ 5 files changed, 145 insertions(+), 11 deletions(-) diff --git a/api.php b/api.php index ff92a6e..551454e 100644 --- a/api.php +++ b/api.php @@ -4384,12 +4384,33 @@ switch ($action) { $vlans = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'vlan')); $ips = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'ip' || $e['tyyppi'] === 'subnet')); - // Rikasta liittymädata laitetiedoilla + // Rikasta liittymädata laitetiedoilla ja IPAM-tiedoilla + // Rakenna IPAM-lookup asiakkaan nimen perusteella + $ipamByCustomer = []; + foreach ($ipamAll as $entry) { + $asiakas = $entry['asiakas'] ?? ''; + if ($asiakas) { + $ipamByCustomer[$asiakas][] = $entry; + } + } + foreach ($connections as &$conn) { $deviceName = $conn['laite'] ?? ''; if ($deviceName && isset($deviceMap[$deviceName])) { $conn['device_info'] = $deviceMap[$deviceName]; } + // Auto-populate VLAN ja IP IPAMista asiakkaan nimen perusteella + $custName = $conn['customer_name'] ?? ''; + $ipamEntries = $ipamByCustomer[$custName] ?? []; + $conn['ipam_vlans'] = []; + $conn['ipam_ips'] = []; + foreach ($ipamEntries as $ie) { + if ($ie['tyyppi'] === 'vlan') { + $conn['ipam_vlans'][] = ['vlan_id' => $ie['vlan_id'], 'nimi' => $ie['nimi'], 'site_name' => $ie['site_name'] ?? '']; + } elseif ($ie['tyyppi'] === 'ip' || $ie['tyyppi'] === 'subnet') { + $conn['ipam_ips'][] = ['verkko' => $ie['verkko'], 'nimi' => $ie['nimi'], 'tila' => $ie['tila'], 'site_name' => $ie['site_name'] ?? '']; + } + } } echo json_encode([ @@ -4421,6 +4442,22 @@ switch ($action) { echo json_encode(['error' => 'Liittymää ei löytynyt']); break; } + // Lisää IPAM-data asiakkaan nimen perusteella + $ipamAll = dbLoadIpam($companyId); + $custName = $conn['customer_name'] ?? ''; + $conn['ipam_vlans'] = []; + $conn['ipam_ips'] = []; + if ($custName) { + foreach ($ipamAll as $ie) { + $asiakas = $ie['asiakas'] ?? ''; + if ($asiakas !== $custName) continue; + if ($ie['tyyppi'] === 'vlan') { + $conn['ipam_vlans'][] = ['vlan_id' => $ie['vlan_id'], 'nimi' => $ie['nimi'], 'site_name' => $ie['site_name'] ?? '']; + } elseif ($ie['tyyppi'] === 'ip' || $ie['tyyppi'] === 'subnet') { + $conn['ipam_ips'][] = ['verkko' => $ie['verkko'], 'nimi' => $ie['nimi'], 'tila' => $ie['tila'], 'site_name' => $ie['site_name'] ?? '']; + } + } + } echo json_encode($conn); } catch (Exception $e) { http_response_code(500); diff --git a/db.php b/db.php index 2c0b69e..750789b 100644 --- a/db.php +++ b/db.php @@ -616,6 +616,7 @@ function initDatabase(): void { "ALTER TABLE document_versions ADD COLUMN content MEDIUMTEXT DEFAULT NULL AFTER mime_type", "ALTER TABLE devices ADD COLUMN laitetila_id VARCHAR(20) DEFAULT NULL AFTER site_id", "ALTER TABLE document_folders ADD COLUMN customer_id VARCHAR(20) DEFAULT NULL AFTER company_id", + "ALTER TABLE customer_connections ADD COLUMN gateway_device_id VARCHAR(20) DEFAULT NULL AFTER ip", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } @@ -2051,9 +2052,13 @@ function dbLoadAllConnections(string $companyId): array { c.yhteyshenkilö AS customer_contact, c.puhelin AS customer_phone, c.sahkoposti AS customer_email, - c.id AS customer_id + c.id AS customer_id, + gw.nimi AS gateway_name, + gw.hallintaosoite AS gateway_ip, + gw.malli AS gateway_model FROM customer_connections cc JOIN customers c ON c.id = cc.customer_id + LEFT JOIN devices gw ON cc.gateway_device_id = gw.id WHERE c.company_id = :companyId ORDER BY cc.kaupunki, cc.asennusosoite ", ['companyId' => $companyId]); @@ -2061,9 +2066,11 @@ function dbLoadAllConnections(string $companyId): array { function dbLoadConnection(int $connectionId): ?array { return _dbFetchOne(" - SELECT cc.*, c.yritys AS customer_name, c.company_id + SELECT cc.*, c.yritys AS customer_name, c.company_id, + gw.nimi AS gateway_name, gw.hallintaosoite AS gateway_ip, gw.malli AS gateway_model FROM customer_connections cc JOIN customers c ON c.id = cc.customer_id + LEFT JOIN devices gw ON cc.gateway_device_id = gw.id WHERE cc.id = ? ", [$connectionId]); } @@ -2071,7 +2078,8 @@ function dbLoadConnection(int $connectionId): ?array { function dbUpdateConnection(int $connectionId, array $data): void { _dbExecute("UPDATE customer_connections SET liittymanopeus = ?, vlan = ?, laite = ?, portti = ?, ip = ?, - asennusosoite = ?, postinumero = ?, kaupunki = ? + asennusosoite = ?, postinumero = ?, kaupunki = ?, + gateway_device_id = ? WHERE id = ?", [ $data['liittymanopeus'] ?? '', $data['vlan'] ?? '', @@ -2081,6 +2089,7 @@ function dbUpdateConnection(int $connectionId, array $data): void { $data['asennusosoite'] ?? '', $data['postinumero'] ?? '', $data['kaupunki'] ?? '', + !empty($data['gateway_device_id']) ? $data['gateway_device_id'] : null, $connectionId ]); } diff --git a/index.html b/index.html index 10d20cd..7a6cb61 100644 --- a/index.html +++ b/index.html @@ -923,6 +923,7 @@ Laite Portti IP + Gateway @@ -1002,6 +1003,14 @@
+
+ +
+ + +
+
+
diff --git a/script.js b/script.js index a118d90..a34d2c2 100644 --- a/script.js +++ b/script.js @@ -4539,7 +4539,7 @@ function renderNetadminTable() { filtered = filtered.filter(c => { const searchStr = [ c.customer_name, c.asennusosoite, c.kaupunki, c.postinumero, - c.liittymanopeus, c.vlan, c.laite, c.portti, c.ip + c.liittymanopeus, c.vlan, c.laite, c.portti, c.ip, c.gateway_name ].filter(Boolean).join(' ').toLowerCase(); return searchStr.includes(query); }); @@ -4568,15 +4568,35 @@ function renderNetadminTable() { deviceInfo?.ping_status === 'down' ? 'netadmin-status-down' : ''; const deviceDisplay = c.laite ? `${esc(c.laite)}` : '-'; + // VLAN: näytä tallennettu arvo, tai IPAM:sta haetut + let vlanDisplay = esc(c.vlan || ''); + if (!c.vlan && c.ipam_vlans && c.ipam_vlans.length > 0) { + vlanDisplay = c.ipam_vlans.map(v => + `${esc(String(v.vlan_id))}` + ).join(', '); + } + + // IP: näytä tallennettu arvo, tai IPAM:sta haetut + let ipDisplay = c.ip ? `${esc(c.ip)}` : ''; + if (!c.ip && c.ipam_ips && c.ipam_ips.length > 0) { + ipDisplay = c.ipam_ips.map(i => + `${esc(i.verkko)}` + ).join(', '); + } + + // Gateway + const gwDisplay = c.gateway_name ? esc(c.gateway_name) : '-'; + return ` ${esc(c.customer_name || '-')} ${esc(addr)} ${esc(c.kaupunki || '-')} ${esc(c.liittymanopeus || '-')} - ${esc(c.vlan || '-')} + ${vlanDisplay || '-'} ${deviceDisplay} ${esc(c.portti || '-')} - ${esc(c.ip || '-')} + ${ipDisplay || '-'} + ${gwDisplay} `; }).join(''); } @@ -4758,6 +4778,20 @@ function getIpComboOptions(source) { return items; } +// Rakennetaan Gateway-combobox optiot (kaikki laitteet devices-listasta) +function getGatewayComboOptions() { + const devices = netadminData.devices || []; + return [...devices].sort((a, b) => (a.nimi || '').localeCompare(b.nimi || '')).map(d => { + const pingDot = d.ping_status === 'up' ? '🟢' : d.ping_status === 'down' ? '🔴' : ''; + return { + value: String(d.id), + label: (pingDot ? pingDot + ' ' : '') + d.nimi, + sub: [d.hallintaosoite, d.malli].filter(Boolean).join(' — '), + searchStr: `${d.nimi} ${d.hallintaosoite || ''} ${d.malli || ''} ${d.funktio || ''} ${d.site_name || ''}`, + }; + }); +} + async function openNetadminDetail(connId) { try { const conn = await apiCall(`netadmin_connection&id=${connId}`); @@ -4777,11 +4811,44 @@ async function openNetadminDetail(connId) { speedSel.insertBefore(opt, speedSel.lastElementChild); } speedSel.value = speed; - // Populoi VLAN, Laite ja IP hakukentät IPAM/Tekniikka-datasta - initCombo(document.getElementById('na-combo-vlan'), getVlanComboOptions(), conn.vlan || ''); + // Populoi VLAN, Laite ja IP hakukentät — yhdistä IPAM-data asiakkaan tietoihin + // Jos asiakkaalla on IPAM VLANeja, näytä ne ensin + let vlanOptions = getVlanComboOptions(); + if (conn.ipam_vlans && conn.ipam_vlans.length > 0) { + const ipamVlanOpts = conn.ipam_vlans.map(v => ({ + value: String(v.vlan_id || ''), + label: String(v.vlan_id || '') + (v.nimi ? ` — ${v.nimi}` : ''), + sub: v.site_name || '', + badge: 'IPAM', badgeClass: 'free', + group: '📌 Asiakkaan VLANit (IPAM)', + searchStr: `${v.vlan_id} ${v.nimi || ''} ${v.site_name || ''}`, + })); + vlanOptions = [...ipamVlanOpts, ...vlanOptions]; + } + initCombo(document.getElementById('na-combo-vlan'), vlanOptions, conn.vlan || ''); + initCombo(document.getElementById('na-combo-laite'), getDeviceComboOptions(), conn.laite || ''); - initCombo(document.getElementById('na-combo-ip'), getIpComboOptions(), conn.ip || ''); + + // Jos asiakkaalla on IPAM IP:itä, näytä ne ensin + let ipOptions = getIpComboOptions(); + if (conn.ipam_ips && conn.ipam_ips.length > 0) { + const ipamIpOpts = conn.ipam_ips.map(i => ({ + value: i.verkko, + label: i.verkko, + sub: [i.nimi, i.site_name].filter(Boolean).join(' — '), + badge: 'IPAM', badgeClass: 'free', + group: '📌 Asiakkaan IP:t (IPAM)', + searchStr: `${i.verkko} ${i.nimi || ''} ${i.site_name || ''}`, + })); + ipOptions = [...ipamIpOpts, ...ipOptions]; + } + initCombo(document.getElementById('na-combo-ip'), ipOptions, conn.ip || ''); + document.getElementById('na-edit-portti').value = conn.portti || ''; + + // Gateway-laitevalitsin + initCombo(document.getElementById('na-combo-gateway'), getGatewayComboOptions(), conn.gateway_device_id ? String(conn.gateway_device_id) : ''); + document.getElementById('netadmin-detail-modal').style.display = ''; } catch (e) { alert('Liittymän avaus epäonnistui: ' + e.message); } } @@ -4800,6 +4867,7 @@ document.getElementById('netadmin-detail-form')?.addEventListener('submit', asyn e.preventDefault(); const connId = document.getElementById('na-edit-id').value; try { + const gwVal = document.getElementById('na-edit-gateway').value; await apiCall('netadmin_connection_update', 'POST', { id: parseInt(connId), asennusosoite: document.getElementById('na-edit-osoite').value, @@ -4809,7 +4877,8 @@ document.getElementById('netadmin-detail-form')?.addEventListener('submit', asyn vlan: document.getElementById('na-edit-vlan').value, laite: document.getElementById('na-edit-laite').value, portti: document.getElementById('na-edit-portti').value, - ip: document.getElementById('na-edit-ip').value + ip: document.getElementById('na-edit-ip').value, + gateway_device_id: gwVal ? parseInt(gwVal) : null }); closeNetadminDetail(); loadNetadmin(); diff --git a/style.css b/style.css index a828f3e..b668b11 100644 --- a/style.css +++ b/style.css @@ -1873,6 +1873,16 @@ span.empty { font-size: 0.7rem; } +.ipam-hint { + background: #f0fdf4; + color: #166534; + padding: 1px 6px; + border-radius: 4px; + font-size: 0.8rem; + border: 1px solid #bbf7d0; + cursor: help; +} + #netadmin-table code { background: #f3f4f6; padding: 1px 5px;