diff --git a/api.php b/api.php
index 32fc7ab..6b0f760 100644
--- a/api.php
+++ b/api.php
@@ -4235,6 +4235,37 @@ switch ($action) {
}
break;
+ // ==================== NETADMIN ====================
+
+ case 'netadmin_connections':
+ requireAuth();
+ $companyId = requireCompany();
+ try {
+ $connections = dbLoadAllConnections($companyId);
+ // Hae myös laitteet ja IPAM-tiedot aggregointia varten
+ $devices = _dbFetchAll("SELECT id, nimi, hallintaosoite, site_id, malli, ping_status FROM devices WHERE company_id = ?", [$companyId]);
+ $deviceMap = [];
+ foreach ($devices as $d) { $deviceMap[$d['nimi']] = $d; }
+
+ // Rikasta liittymädata laitetiedoilla
+ foreach ($connections as &$conn) {
+ $deviceName = $conn['laite'] ?? '';
+ if ($deviceName && isset($deviceMap[$deviceName])) {
+ $conn['device_info'] = $deviceMap[$deviceName];
+ }
+ }
+
+ echo json_encode([
+ 'connections' => $connections,
+ 'total' => count($connections),
+ 'devices' => $devices
+ ]);
+ } catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Liittymien haku epäonnistui: ' . $e->getMessage()]);
+ }
+ break;
+
// ==================== LAITETILAT ====================
case 'laitetilat':
diff --git a/db.php b/db.php
index 206433d..c458569 100644
--- a/db.php
+++ b/db.php
@@ -1878,3 +1878,20 @@ function dbDeleteLaitetilaFile(string $fileId): ?array {
}
return $file;
}
+
+// ==================== NETADMIN ====================
+
+function dbLoadAllConnections(string $companyId): array {
+ return _dbFetchAll("
+ SELECT cc.*,
+ c.yritys AS customer_name,
+ c.yhteyshenkilö AS customer_contact,
+ c.puhelin AS customer_phone,
+ c.sahkoposti AS customer_email,
+ c.id AS customer_id
+ FROM customer_connections cc
+ JOIN customers c ON c.id = cc.customer_id
+ WHERE c.company_id = :companyId
+ ORDER BY cc.kaupunki, cc.asennusosoite
+ ", ['companyId' => $companyId]);
+}
diff --git a/index.html b/index.html
index eed0fcf..abb07bb 100644
--- a/index.html
+++ b/index.html
@@ -84,6 +84,7 @@
+
@@ -963,6 +964,52 @@
+
+
+
+
+
🌐 NetAdmin — Liittymät
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Asiakas |
+ Osoite |
+ Kaupunki |
+ Nopeus |
+ VLAN |
+ Laite |
+ Portti |
+ IP |
+ Hinta |
+
+
+
+
+
+
+
+
+
@@ -1405,6 +1452,9 @@
+
diff --git a/script.js b/script.js
index 8fb5cf5..29d2771 100644
--- a/script.js
+++ b/script.js
@@ -200,7 +200,7 @@ async function showDashboard() {
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
const hash = window.location.hash.replace('#', '');
const [mainHash, subHash] = hash.split('/');
- const validTabs = ['customers', 'leads', 'tekniikka', 'ohjeet', 'todo', 'documents', 'laitetilat', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
+ const validTabs = ['customers', 'leads', 'tekniikka', 'ohjeet', 'todo', 'documents', 'laitetilat', 'netadmin', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
const startTab = validTabs.includes(mainHash) ? mainHash : 'customers';
switchToTab(startTab, subHash);
}
@@ -265,6 +265,7 @@ function switchToTab(target, subTab) {
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
if (target === 'documents') { loadDocuments(); showDocsListView(); }
if (target === 'laitetilat') { loadLaitetilat(); showLaitetilatListView(); }
+ if (target === 'netadmin') loadNetadmin();
if (target === 'users') loadUsers();
if (target === 'settings') loadSettings();
if (target === 'companies') loadCompaniesTab();
@@ -4394,6 +4395,106 @@ document.getElementById('btn-time-cancel')?.addEventListener('click', () => {
});
document.getElementById('btn-time-save')?.addEventListener('click', () => addTimeEntry());
+// ==================== NETADMIN ====================
+
+let netadminData = { connections: [], devices: [] };
+
+async function loadNetadmin() {
+ try {
+ netadminData = await apiCall('netadmin_connections');
+ populateNetadminFilters();
+ renderNetadminTable();
+ } catch (e) { console.error('NetAdmin lataus epäonnistui:', e); }
+}
+
+function populateNetadminFilters() {
+ const conns = netadminData.connections || [];
+
+ // Kaupungit
+ const cities = [...new Set(conns.map(c => c.kaupunki).filter(Boolean))].sort();
+ const citySel = document.getElementById('netadmin-filter-city');
+ const cityVal = citySel.value;
+ citySel.innerHTML = '' +
+ cities.map(c => ``).join('');
+ citySel.value = cityVal;
+
+ // Nopeudet
+ const speeds = [...new Set(conns.map(c => c.liittymanopeus).filter(Boolean))].sort();
+ const speedSel = document.getElementById('netadmin-filter-speed');
+ const speedVal = speedSel.value;
+ speedSel.innerHTML = '' +
+ speeds.map(s => ``).join('');
+ speedSel.value = speedVal;
+
+ // Laitteet
+ const devs = [...new Set(conns.map(c => c.laite).filter(Boolean))].sort();
+ const devSel = document.getElementById('netadmin-filter-device');
+ const devVal = devSel.value;
+ devSel.innerHTML = '' +
+ devs.map(d => ``).join('');
+ devSel.value = devVal;
+}
+
+function renderNetadminTable() {
+ const query = (document.getElementById('netadmin-search')?.value || '').toLowerCase().trim();
+ const filterCity = document.getElementById('netadmin-filter-city')?.value || '';
+ const filterSpeed = document.getElementById('netadmin-filter-speed')?.value || '';
+ const filterDevice = document.getElementById('netadmin-filter-device')?.value || '';
+
+ let filtered = netadminData.connections || [];
+
+ if (query) {
+ 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
+ ].filter(Boolean).join(' ').toLowerCase();
+ return searchStr.includes(query);
+ });
+ }
+ if (filterCity) filtered = filtered.filter(c => c.kaupunki === filterCity);
+ if (filterSpeed) filtered = filtered.filter(c => c.liittymanopeus === filterSpeed);
+ if (filterDevice) filtered = filtered.filter(c => c.laite === filterDevice);
+
+ const tbody = document.getElementById('netadmin-tbody');
+ const noEl = document.getElementById('no-netadmin');
+ const countEl = document.getElementById('netadmin-count');
+
+ countEl.textContent = `${filtered.length} / ${(netadminData.connections || []).length} liittymää`;
+
+ if (filtered.length === 0) {
+ tbody.innerHTML = '';
+ noEl.style.display = '';
+ return;
+ }
+ noEl.style.display = 'none';
+
+ tbody.innerHTML = filtered.map(c => {
+ const addr = c.asennusosoite || '-';
+ const deviceInfo = c.device_info;
+ const pingClass = deviceInfo?.ping_status === 'up' ? 'netadmin-status-up' :
+ deviceInfo?.ping_status === 'down' ? 'netadmin-status-down' : '';
+ const deviceDisplay = c.laite ? `${esc(c.laite)}` : '-';
+
+ return `
+ | ${esc(c.customer_name || '-')} |
+ ${esc(addr)} |
+ ${esc(c.kaupunki || '-')} |
+ ${esc(c.liittymanopeus || '-')} |
+ ${esc(c.vlan || '-')} |
+ ${deviceDisplay} |
+ ${esc(c.portti || '-')} |
+ ${esc(c.ip || '-')} |
+ ${c.hinta ? parseFloat(c.hinta).toFixed(2) + ' €' : '-'} |
+
`;
+ }).join('');
+}
+
+document.getElementById('netadmin-search')?.addEventListener('input', renderNetadminTable);
+document.getElementById('netadmin-filter-city')?.addEventListener('change', renderNetadminTable);
+document.getElementById('netadmin-filter-speed')?.addEventListener('change', renderNetadminTable);
+document.getElementById('netadmin-filter-device')?.addEventListener('change', renderNetadminTable);
+
// ==================== DOKUMENTIT ====================
let allDocuments = [];
@@ -4910,7 +5011,7 @@ document.getElementById('laitetila-edit-form')?.addEventListener('submit', async
// ==================== MODUULIT ====================
-const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'ohjeet', 'todo', 'documents', 'laitetilat', 'archive', 'changelog', 'settings'];
+const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'ohjeet', 'todo', 'documents', 'laitetilat', 'netadmin', 'archive', 'changelog', 'settings'];
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
function applyModules(modules) {
diff --git a/style.css b/style.css
index c17586b..321b3d0 100644
--- a/style.css
+++ b/style.css
@@ -1708,3 +1708,39 @@ span.empty {
.btn-icon:hover {
background: #fee2e2;
}
+
+/* ==================== NETADMIN ==================== */
+.netadmin-speed {
+ display: inline-block;
+ padding: 2px 8px;
+ background: #dbeafe;
+ color: #1e40af;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+
+.netadmin-status-up {
+ color: #059669;
+}
+.netadmin-status-up::before {
+ content: '●';
+ margin-right: 4px;
+ font-size: 0.7rem;
+}
+
+.netadmin-status-down {
+ color: #dc2626;
+}
+.netadmin-status-down::before {
+ content: '●';
+ margin-right: 4px;
+ font-size: 0.7rem;
+}
+
+#netadmin-table code {
+ background: #f3f4f6;
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-size: 0.82rem;
+}