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:
31
api.php
31
api.php
@@ -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
17
db.php
@@ -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]);
|
||||||
|
}
|
||||||
|
|||||||
50
index.html
50
index.html
@@ -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
105
script.js
@@ -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) {
|
||||||
|
|||||||
36
style.css
36
style.css
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user