NetAdmin: Gateway-kenttä, IPAM-integraatio VLAN/IP-tietoihin
- 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 <noreply@anthropic.com>
This commit is contained in:
39
api.php
39
api.php
@@ -4384,12 +4384,33 @@ switch ($action) {
|
|||||||
$vlans = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'vlan'));
|
$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'));
|
$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) {
|
foreach ($connections as &$conn) {
|
||||||
$deviceName = $conn['laite'] ?? '';
|
$deviceName = $conn['laite'] ?? '';
|
||||||
if ($deviceName && isset($deviceMap[$deviceName])) {
|
if ($deviceName && isset($deviceMap[$deviceName])) {
|
||||||
$conn['device_info'] = $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([
|
echo json_encode([
|
||||||
@@ -4421,6 +4442,22 @@ switch ($action) {
|
|||||||
echo json_encode(['error' => 'Liittymää ei löytynyt']);
|
echo json_encode(['error' => 'Liittymää ei löytynyt']);
|
||||||
break;
|
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);
|
echo json_encode($conn);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|||||||
15
db.php
15
db.php
@@ -616,6 +616,7 @@ function initDatabase(): void {
|
|||||||
"ALTER TABLE document_versions ADD COLUMN content MEDIUMTEXT DEFAULT NULL AFTER mime_type",
|
"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 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 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) {
|
foreach ($alters as $sql) {
|
||||||
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
|
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.yhteyshenkilö AS customer_contact,
|
||||||
c.puhelin AS customer_phone,
|
c.puhelin AS customer_phone,
|
||||||
c.sahkoposti AS customer_email,
|
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
|
FROM customer_connections cc
|
||||||
JOIN customers c ON c.id = cc.customer_id
|
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
|
WHERE c.company_id = :companyId
|
||||||
ORDER BY cc.kaupunki, cc.asennusosoite
|
ORDER BY cc.kaupunki, cc.asennusosoite
|
||||||
", ['companyId' => $companyId]);
|
", ['companyId' => $companyId]);
|
||||||
@@ -2061,9 +2066,11 @@ function dbLoadAllConnections(string $companyId): array {
|
|||||||
|
|
||||||
function dbLoadConnection(int $connectionId): ?array {
|
function dbLoadConnection(int $connectionId): ?array {
|
||||||
return _dbFetchOne("
|
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
|
FROM customer_connections cc
|
||||||
JOIN customers c ON c.id = cc.customer_id
|
JOIN customers c ON c.id = cc.customer_id
|
||||||
|
LEFT JOIN devices gw ON cc.gateway_device_id = gw.id
|
||||||
WHERE cc.id = ?
|
WHERE cc.id = ?
|
||||||
", [$connectionId]);
|
", [$connectionId]);
|
||||||
}
|
}
|
||||||
@@ -2071,7 +2078,8 @@ function dbLoadConnection(int $connectionId): ?array {
|
|||||||
function dbUpdateConnection(int $connectionId, array $data): void {
|
function dbUpdateConnection(int $connectionId, array $data): void {
|
||||||
_dbExecute("UPDATE customer_connections SET
|
_dbExecute("UPDATE customer_connections SET
|
||||||
liittymanopeus = ?, vlan = ?, laite = ?, portti = ?, ip = ?,
|
liittymanopeus = ?, vlan = ?, laite = ?, portti = ?, ip = ?,
|
||||||
asennusosoite = ?, postinumero = ?, kaupunki = ?
|
asennusosoite = ?, postinumero = ?, kaupunki = ?,
|
||||||
|
gateway_device_id = ?
|
||||||
WHERE id = ?", [
|
WHERE id = ?", [
|
||||||
$data['liittymanopeus'] ?? '',
|
$data['liittymanopeus'] ?? '',
|
||||||
$data['vlan'] ?? '',
|
$data['vlan'] ?? '',
|
||||||
@@ -2081,6 +2089,7 @@ function dbUpdateConnection(int $connectionId, array $data): void {
|
|||||||
$data['asennusosoite'] ?? '',
|
$data['asennusosoite'] ?? '',
|
||||||
$data['postinumero'] ?? '',
|
$data['postinumero'] ?? '',
|
||||||
$data['kaupunki'] ?? '',
|
$data['kaupunki'] ?? '',
|
||||||
|
!empty($data['gateway_device_id']) ? $data['gateway_device_id'] : null,
|
||||||
$connectionId
|
$connectionId
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -923,6 +923,7 @@
|
|||||||
<th>Laite</th>
|
<th>Laite</th>
|
||||||
<th>Portti</th>
|
<th>Portti</th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
|
<th>Gateway</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="netadmin-tbody"></tbody>
|
<tbody id="netadmin-tbody"></tbody>
|
||||||
@@ -1002,6 +1003,14 @@
|
|||||||
<div class="combo-list"></div>
|
<div class="combo-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gateway</label>
|
||||||
|
<div class="combo-wrap" id="na-combo-gateway">
|
||||||
|
<input type="text" placeholder="Hae gateway-laitetta..." autocomplete="off">
|
||||||
|
<input type="hidden" id="na-edit-gateway">
|
||||||
|
<div class="combo-list"></div>
|
||||||
|
</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;">
|
||||||
<button type="button" class="btn-secondary" onclick="closeNetadminDetail()">Sulje</button>
|
<button type="button" class="btn-secondary" onclick="closeNetadminDetail()">Sulje</button>
|
||||||
|
|||||||
83
script.js
83
script.js
@@ -4539,7 +4539,7 @@ function renderNetadminTable() {
|
|||||||
filtered = filtered.filter(c => {
|
filtered = filtered.filter(c => {
|
||||||
const searchStr = [
|
const searchStr = [
|
||||||
c.customer_name, c.asennusosoite, c.kaupunki, c.postinumero,
|
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();
|
].filter(Boolean).join(' ').toLowerCase();
|
||||||
return searchStr.includes(query);
|
return searchStr.includes(query);
|
||||||
});
|
});
|
||||||
@@ -4568,15 +4568,35 @@ function renderNetadminTable() {
|
|||||||
deviceInfo?.ping_status === 'down' ? 'netadmin-status-down' : '';
|
deviceInfo?.ping_status === 'down' ? 'netadmin-status-down' : '';
|
||||||
const deviceDisplay = c.laite ? `<span class="${pingClass}">${esc(c.laite)}</span>` : '-';
|
const deviceDisplay = c.laite ? `<span class="${pingClass}">${esc(c.laite)}</span>` : '-';
|
||||||
|
|
||||||
|
// 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 =>
|
||||||
|
`<span class="ipam-hint" title="IPAM: ${esc(v.nimi || '')}">${esc(String(v.vlan_id))}</span>`
|
||||||
|
).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP: näytä tallennettu arvo, tai IPAM:sta haetut
|
||||||
|
let ipDisplay = c.ip ? `<code>${esc(c.ip)}</code>` : '';
|
||||||
|
if (!c.ip && c.ipam_ips && c.ipam_ips.length > 0) {
|
||||||
|
ipDisplay = c.ipam_ips.map(i =>
|
||||||
|
`<span class="ipam-hint" title="IPAM: ${esc(i.nimi || '')}">${esc(i.verkko)}</span>`
|
||||||
|
).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gateway
|
||||||
|
const gwDisplay = c.gateway_name ? esc(c.gateway_name) : '-';
|
||||||
|
|
||||||
return `<tr onclick="openNetadminDetail(${c.id})" style="cursor:pointer;" title="Avaa liittymän tiedot">
|
return `<tr onclick="openNetadminDetail(${c.id})" style="cursor:pointer;" title="Avaa liittymän tiedot">
|
||||||
<td><strong>${esc(c.customer_name || '-')}</strong></td>
|
<td><strong>${esc(c.customer_name || '-')}</strong></td>
|
||||||
<td>${esc(addr)}</td>
|
<td>${esc(addr)}</td>
|
||||||
<td>${esc(c.kaupunki || '-')}</td>
|
<td>${esc(c.kaupunki || '-')}</td>
|
||||||
<td><span class="netadmin-speed">${esc(c.liittymanopeus || '-')}</span></td>
|
<td><span class="netadmin-speed">${esc(c.liittymanopeus || '-')}</span></td>
|
||||||
<td>${esc(c.vlan || '-')}</td>
|
<td>${vlanDisplay || '-'}</td>
|
||||||
<td>${deviceDisplay}</td>
|
<td>${deviceDisplay}</td>
|
||||||
<td>${esc(c.portti || '-')}</td>
|
<td>${esc(c.portti || '-')}</td>
|
||||||
<td><code>${esc(c.ip || '-')}</code></td>
|
<td>${ipDisplay || '-'}</td>
|
||||||
|
<td>${gwDisplay}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -4758,6 +4778,20 @@ function getIpComboOptions(source) {
|
|||||||
return items;
|
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) {
|
async function openNetadminDetail(connId) {
|
||||||
try {
|
try {
|
||||||
const conn = await apiCall(`netadmin_connection&id=${connId}`);
|
const conn = await apiCall(`netadmin_connection&id=${connId}`);
|
||||||
@@ -4777,11 +4811,44 @@ async function openNetadminDetail(connId) {
|
|||||||
speedSel.insertBefore(opt, speedSel.lastElementChild);
|
speedSel.insertBefore(opt, speedSel.lastElementChild);
|
||||||
}
|
}
|
||||||
speedSel.value = speed;
|
speedSel.value = speed;
|
||||||
// Populoi VLAN, Laite ja IP hakukentät IPAM/Tekniikka-datasta
|
// Populoi VLAN, Laite ja IP hakukentät — yhdistä IPAM-data asiakkaan tietoihin
|
||||||
initCombo(document.getElementById('na-combo-vlan'), getVlanComboOptions(), conn.vlan || '');
|
// 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-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 || '';
|
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 = '';
|
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); }
|
||||||
}
|
}
|
||||||
@@ -4800,6 +4867,7 @@ document.getElementById('netadmin-detail-form')?.addEventListener('submit', asyn
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const connId = document.getElementById('na-edit-id').value;
|
const connId = document.getElementById('na-edit-id').value;
|
||||||
try {
|
try {
|
||||||
|
const gwVal = document.getElementById('na-edit-gateway').value;
|
||||||
await apiCall('netadmin_connection_update', 'POST', {
|
await apiCall('netadmin_connection_update', 'POST', {
|
||||||
id: parseInt(connId),
|
id: parseInt(connId),
|
||||||
asennusosoite: document.getElementById('na-edit-osoite').value,
|
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,
|
vlan: document.getElementById('na-edit-vlan').value,
|
||||||
laite: document.getElementById('na-edit-laite').value,
|
laite: document.getElementById('na-edit-laite').value,
|
||||||
portti: document.getElementById('na-edit-portti').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();
|
closeNetadminDetail();
|
||||||
loadNetadmin();
|
loadNetadmin();
|
||||||
|
|||||||
10
style.css
10
style.css
@@ -1873,6 +1873,16 @@ span.empty {
|
|||||||
font-size: 0.7rem;
|
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 {
|
#netadmin-table code {
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
|
|||||||
Reference in New Issue
Block a user