NetAdmin-moduuli: liittymien listaus ja haku

Kokoaa kaikki asiakkaiden liittymät yhteen näkymään haulla ja suodattimilla.
Sarakkeet: asiakas, osoite, kaupunki, nopeus, VLAN, laite, portti, IP, hinta.
Suodattimet: kaupunki, nopeus, laite. Laitetietojen ping-status näkyvissä.
Klikkaus avaa asiakkaan muokkaukseen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 16:39:24 +02:00
parent e6fa65165e
commit f05313530f
5 changed files with 237 additions and 2 deletions

31
api.php
View File

@@ -4235,6 +4235,37 @@ switch ($action) {
} }
break; 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 ==================== // ==================== LAITETILAT ====================
case 'laitetilat': case 'laitetilat':

17
db.php
View File

@@ -1878,3 +1878,20 @@ function dbDeleteLaitetilaFile(string $fileId): ?array {
} }
return $file; 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]);
}

View File

@@ -84,6 +84,7 @@
<button class="tab" data-tab="tekniikka">Tekniikka</button> <button class="tab" data-tab="tekniikka">Tekniikka</button>
<button class="tab" data-tab="documents">Dokumentit</button> <button class="tab" data-tab="documents">Dokumentit</button>
<button class="tab" data-tab="laitetilat">Laitetilat</button> <button class="tab" data-tab="laitetilat">Laitetilat</button>
<button class="tab" data-tab="netadmin">NetAdmin</button>
<button class="tab" data-tab="ohjeet">Ohjeet</button> <button class="tab" data-tab="ohjeet">Ohjeet</button>
<button class="tab" data-tab="archive">Arkisto</button> <button class="tab" data-tab="archive">Arkisto</button>
<button class="tab" data-tab="changelog">Muutosloki</button> <button class="tab" data-tab="changelog">Muutosloki</button>
@@ -963,6 +964,52 @@
</div> </div>
</div> </div>
<!-- Tab: NetAdmin -->
<div class="tab-content" id="tab-content-netadmin">
<div class="main-container">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem;">
<h3 style="color:var(--primary-dark);margin:0;">🌐 NetAdmin — Liittymät</h3>
<div style="display:flex;gap:0.5rem;align-items:center;">
<span id="netadmin-count" style="font-size:0.85rem;color:#888;"></span>
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;">
<input type="text" id="netadmin-search" placeholder="Hae osoite, asiakas, IP, VLAN, laite, portti..." style="flex:1;min-width:200px;">
<select id="netadmin-filter-city" style="min-width:130px;">
<option value="">Kaikki kaupungit</option>
</select>
<select id="netadmin-filter-speed" style="min-width:130px;">
<option value="">Kaikki nopeudet</option>
</select>
<select id="netadmin-filter-device" style="min-width:130px;">
<option value="">Kaikki laitteet</option>
</select>
</div>
<div class="table-card">
<table id="netadmin-table">
<thead>
<tr>
<th>Asiakas</th>
<th>Osoite</th>
<th>Kaupunki</th>
<th>Nopeus</th>
<th>VLAN</th>
<th>Laite</th>
<th>Portti</th>
<th>IP</th>
<th>Hinta</th>
</tr>
</thead>
<tbody id="netadmin-tbody"></tbody>
</table>
<div id="no-netadmin" class="empty-state" style="display:none;">
<div class="empty-icon">🌐</div>
<p>Ei liittymiä.</p>
</div>
</div>
</div>
</div>
<!-- Tab: Muutosloki --> <!-- Tab: Muutosloki -->
<div class="tab-content" id="tab-content-changelog"> <div class="tab-content" id="tab-content-changelog">
<div class="main-container"> <div class="main-container">
@@ -1405,6 +1452,9 @@
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" data-module="laitetilat"> Laitetilat <input type="checkbox" data-module="laitetilat"> Laitetilat
</label> </label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" data-module="netadmin"> NetAdmin
</label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" data-module="settings" checked> Asetukset / API <input type="checkbox" data-module="settings" checked> Asetukset / API
</label> </label>

105
script.js
View File

@@ -200,7 +200,7 @@ async function showDashboard() {
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks) // Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
const hash = window.location.hash.replace('#', ''); const hash = window.location.hash.replace('#', '');
const [mainHash, subHash] = hash.split('/'); 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'; const startTab = validTabs.includes(mainHash) ? mainHash : 'customers';
switchToTab(startTab, subHash); 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 === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
if (target === 'documents') { loadDocuments(); showDocsListView(); } if (target === 'documents') { loadDocuments(); showDocsListView(); }
if (target === 'laitetilat') { loadLaitetilat(); showLaitetilatListView(); } if (target === 'laitetilat') { loadLaitetilat(); showLaitetilatListView(); }
if (target === 'netadmin') loadNetadmin();
if (target === 'users') loadUsers(); if (target === 'users') loadUsers();
if (target === 'settings') loadSettings(); if (target === 'settings') loadSettings();
if (target === 'companies') loadCompaniesTab(); if (target === 'companies') loadCompaniesTab();
@@ -4394,6 +4395,106 @@ document.getElementById('btn-time-cancel')?.addEventListener('click', () => {
}); });
document.getElementById('btn-time-save')?.addEventListener('click', () => addTimeEntry()); 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 = '<option value="">Kaikki kaupungit</option>' +
cities.map(c => `<option value="${c}">${esc(c)}</option>`).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 = '<option value="">Kaikki nopeudet</option>' +
speeds.map(s => `<option value="${s}">${esc(s)}</option>`).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 = '<option value="">Kaikki laitteet</option>' +
devs.map(d => `<option value="${d}">${esc(d)}</option>`).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 ? `<span class="${pingClass}">${esc(c.laite)}</span>` : '-';
return `<tr onclick="editCustomer('${c.customer_id}')" style="cursor:pointer;" title="Avaa asiakas">
<td><strong>${esc(c.customer_name || '-')}</strong></td>
<td>${esc(addr)}</td>
<td>${esc(c.kaupunki || '-')}</td>
<td><span class="netadmin-speed">${esc(c.liittymanopeus || '-')}</span></td>
<td>${esc(c.vlan || '-')}</td>
<td>${deviceDisplay}</td>
<td>${esc(c.portti || '-')}</td>
<td><code>${esc(c.ip || '-')}</code></td>
<td class="price-cell">${c.hinta ? parseFloat(c.hinta).toFixed(2) + ' €' : '-'}</td>
</tr>`;
}).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 ==================== // ==================== DOKUMENTIT ====================
let allDocuments = []; let allDocuments = [];
@@ -4910,7 +5011,7 @@ document.getElementById('laitetila-edit-form')?.addEventListener('submit', async
// ==================== MODUULIT ==================== // ==================== 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']; const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
function applyModules(modules) { function applyModules(modules) {

View File

@@ -1708,3 +1708,39 @@ span.empty {
.btn-icon:hover { .btn-icon:hover {
background: #fee2e2; 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;
}