feat: Tekniikka-moduuli sub-tabeilla (Laitteet + Sijainnit + IPAM)

- Laitteet-tabi → Tekniikka (sub-tabit: Laitteet, Sijainnit, IPAM)
- Sijainnit siirretty omaksi taulukkonäkymäksi (+ "Lisää sijainti" laitteiden yhteydessä)
- Uusi IPAM-näkymä: IP-osoitteet, subnetit ja VLANit hallintaan
- IPAM: tyyppi (subnet/vlan/ip), verkko, VLAN-nro, sijainti, tila, asiakas
- Sub-tab-tyylit ja logiikka
- Yhteensopivuus: vanha 'devices' moduuli → 'tekniikka'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 20:18:56 +02:00
parent e37da2b40d
commit 9140c912cd
5 changed files with 558 additions and 56 deletions

271
script.js
View File

@@ -194,7 +194,7 @@ async function showDashboard() {
populateCompanySelector();
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
const hash = window.location.hash.replace('#', '');
const validTabs = ['customers', 'leads', 'devices', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
const validTabs = ['customers', 'leads', 'tekniikka', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
const startTab = validTabs.includes(hash) ? hash : 'customers';
switchToTab(startTab);
}
@@ -241,7 +241,7 @@ function switchToTab(target) {
// Lataa sisältö tarvittaessa
if (target === 'customers') loadCustomers();
if (target === 'leads') loadLeads();
if (target === 'devices') loadDevices();
if (target === 'tekniikka') { loadDevices(); loadSitesTab(); loadIpam(); }
if (target === 'archive') loadArchive();
if (target === 'changelog') loadChangelog();
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
@@ -2083,8 +2083,11 @@ async function showCompanyDetail(id) {
logoPreview.style.display = 'none';
}
// Moduuli-checkboxit
const enabledMods = comp?.enabled_modules || [];
// Moduuli-checkboxit (yhteensopivuus: vanha 'devices' → 'tekniikka')
let enabledMods = comp?.enabled_modules || [];
if (enabledMods.includes('devices') && !enabledMods.includes('tekniikka')) {
enabledMods = enabledMods.map(m => m === 'devices' ? 'tekniikka' : m);
}
document.querySelectorAll('#modules-checkboxes input[data-module]').forEach(cb => {
const mod = cb.dataset.module;
// Jos enabled_modules on tyhjä → kaikki päällä (oletus)
@@ -2488,16 +2491,93 @@ document.getElementById('device-form')?.addEventListener('submit', async (e) =>
document.getElementById('device-search-input')?.addEventListener('input', () => renderDevices());
// ==================== SIJAINNIT (SITES) HALLINTA ====================
// ==================== TEKNIIKKA SUB-TABS ====================
async function loadSites() {
function switchSubTab(target) {
document.querySelectorAll('#tab-content-tekniikka .sub-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('#tab-content-tekniikka .sub-tab-content').forEach(c => c.classList.remove('active'));
const btn = document.querySelector(`.sub-tab[data-subtab="${target}"]`);
if (btn) btn.classList.add('active');
const content = document.getElementById('subtab-' + target);
if (content) content.classList.add('active');
}
document.querySelectorAll('#tab-content-tekniikka .sub-tab').forEach(btn => {
btn.addEventListener('click', () => switchSubTab(btn.dataset.subtab));
});
// ==================== SIJAINNIT (SITES) — TEKNIIKKA TAB ====================
let sitesTabData = [];
async function loadSitesTab() {
try {
sitesData = await apiCall('sites');
renderSites();
sitesTabData = sitesData;
renderSitesTab();
renderSitesSettings(); // Päivitä myös asetuksissa
} catch (e) { console.error(e); }
}
function renderSites() {
function renderSitesTab() {
const query = (document.getElementById('site-search-input')?.value || '').toLowerCase().trim();
let filtered = sitesTabData;
if (query) {
filtered = sitesTabData.filter(s =>
(s.nimi || '').toLowerCase().includes(query) ||
(s.osoite || '').toLowerCase().includes(query) ||
(s.kaupunki || '').toLowerCase().includes(query)
);
}
const tbody = document.getElementById('site-tbody');
const noSites = document.getElementById('no-sites-tab');
if (filtered.length === 0) {
tbody.innerHTML = '';
if (noSites) noSites.style.display = 'block';
} else {
if (noSites) noSites.style.display = 'none';
tbody.innerHTML = filtered.map(s => {
const deviceCount = devicesData.filter(d => d.site_id === s.id).length;
return `<tr>
<td><strong>${esc(s.nimi)}</strong></td>
<td>${esc(s.osoite || '-')}</td>
<td>${esc(s.kaupunki || '-')}</td>
<td style="text-align:center;">${deviceCount}</td>
<td class="actions-cell">
<button class="btn-link" onclick="editSiteTab('${s.id}')">✎</button>
<button class="btn-link" style="color:#dc2626;" onclick="deleteSite('${s.id}','${esc(s.nimi)}')">🗑</button>
</td>
</tr>`;
}).join('');
}
document.getElementById('site-count').textContent = filtered.length + ' sijaintia';
}
function editSiteTab(id) {
const s = sitesData.find(x => x.id === id);
if (!s) return;
document.getElementById('site-form-id').value = s.id;
document.getElementById('site-form-nimi').value = s.nimi || '';
document.getElementById('site-form-osoite').value = s.osoite || '';
document.getElementById('site-form-kaupunki').value = s.kaupunki || '';
document.getElementById('site-form-title').textContent = 'Muokkaa sijaintia';
document.getElementById('site-form-container').style.display = '';
}
// Alias vanhalle editSite-funktiolle
function editSite(id) { editSiteTab(id); }
async function deleteSite(id, name) {
if (!confirm(`Poistetaanko sijainti "${name}"? Laitteet joissa tämä sijainti on menettävät sijainti-viittauksen.`)) return;
try {
await apiCall('site_delete', 'POST', { id });
loadSitesTab();
loadDevices();
} catch (e) { alert(e.message); }
}
// Renderöi sijainnit myös asetuksissa (company detail)
function renderSitesSettings() {
const container = document.getElementById('sites-list');
if (!container) return;
if (sitesData.length === 0) {
@@ -2517,24 +2597,33 @@ function renderSites() {
</div>`).join('');
}
function editSite(id) {
const s = sitesData.find(x => x.id === id);
if (!s) return;
document.getElementById('site-form-id').value = s.id;
document.getElementById('site-form-nimi').value = s.nimi || '';
document.getElementById('site-form-osoite').value = s.osoite || '';
document.getElementById('site-form-kaupunki').value = s.kaupunki || '';
document.getElementById('site-form-title').textContent = 'Muokkaa sijaintia';
document.getElementById('site-form-container').style.display = '';
}
// Alias loadSites asetuksista kutsuun
async function loadSites() { await loadSitesTab(); }
async function deleteSite(id, name) {
if (!confirm(`Poistetaanko sijainti "${name}"? Laitteet joissa tämä sijainti on menettävät sijainti-viittauksen.`)) return;
try {
await apiCall('site_delete', 'POST', { id });
loadSites();
} catch (e) { alert(e.message); }
}
function renderSites() { renderSitesSettings(); }
document.getElementById('site-search-input')?.addEventListener('input', () => renderSitesTab());
// Lisää sijainti -napit (tekniikka-tab + laitteet-sivun quick-nappi)
document.getElementById('btn-add-site-tab')?.addEventListener('click', () => {
document.getElementById('site-form-id').value = '';
document.getElementById('site-form-nimi').value = '';
document.getElementById('site-form-osoite').value = '';
document.getElementById('site-form-kaupunki').value = '';
document.getElementById('site-form-title').textContent = 'Uusi sijainti';
document.getElementById('site-form-container').style.display = '';
switchSubTab('sites');
});
document.getElementById('btn-add-site-quick')?.addEventListener('click', () => {
document.getElementById('site-form-id').value = '';
document.getElementById('site-form-nimi').value = '';
document.getElementById('site-form-osoite').value = '';
document.getElementById('site-form-kaupunki').value = '';
document.getElementById('site-form-title').textContent = 'Uusi sijainti';
document.getElementById('site-form-container').style.display = '';
switchSubTab('sites');
});
document.getElementById('btn-add-site')?.addEventListener('click', () => {
document.getElementById('site-form-id').value = '';
@@ -2558,7 +2647,7 @@ document.getElementById('btn-save-site')?.addEventListener('click', async () =>
try {
await apiCall('site_save', 'POST', data);
document.getElementById('site-form-container').style.display = 'none';
loadSites();
loadSitesTab();
} catch (e) { alert(e.message); }
});
@@ -2566,12 +2655,140 @@ document.getElementById('btn-cancel-site')?.addEventListener('click', () => {
document.getElementById('site-form-container').style.display = 'none';
});
// ==================== IPAM ====================
let ipamData = [];
async function loadIpam() {
try {
ipamData = await apiCall('ipam');
renderIpam();
} catch (e) { console.error(e); }
}
function renderIpam() {
const query = (document.getElementById('ipam-search-input')?.value || '').toLowerCase().trim();
let filtered = ipamData;
if (query) {
filtered = ipamData.filter(e =>
(e.tyyppi || '').toLowerCase().includes(query) ||
(e.verkko || '').toLowerCase().includes(query) ||
(e.nimi || '').toLowerCase().includes(query) ||
(e.site_name || '').toLowerCase().includes(query) ||
(e.asiakas || '').toLowerCase().includes(query) ||
(e.lisatiedot || '').toLowerCase().includes(query) ||
String(e.vlan_id || '').includes(query)
);
}
const tbody = document.getElementById('ipam-tbody');
const noIpam = document.getElementById('no-ipam');
if (filtered.length === 0) {
tbody.innerHTML = '';
if (noIpam) noIpam.style.display = 'block';
} else {
if (noIpam) noIpam.style.display = 'none';
const tyyppiLabel = { subnet: 'Subnet', vlan: 'VLAN', ip: 'IP' };
const tilaClass = { vapaa: 'ipam-tila-vapaa', varattu: 'ipam-tila-varattu', reserved: 'ipam-tila-reserved' };
const tilaLabel = { vapaa: 'Vapaa', varattu: 'Varattu', reserved: 'Reserved' };
tbody.innerHTML = filtered.map(e => `<tr>
<td><span style="font-weight:600;">${tyyppiLabel[e.tyyppi] || e.tyyppi}</span></td>
<td><code style="font-size:0.85rem;">${esc(e.verkko || '-')}</code></td>
<td>${e.vlan_id ? '<strong>' + e.vlan_id + '</strong>' : '-'}</td>
<td>${esc(e.nimi || '-')}</td>
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">-</span>'}</td>
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
<td>${esc(e.asiakas || '-')}</td>
<td class="actions-cell">
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
</td>
</tr>`).join('');
}
document.getElementById('ipam-count').textContent = filtered.length + ' merkintää' + (query ? ` (${ipamData.length} yhteensä)` : '');
}
async function loadIpamSitesDropdown() {
try {
if (!sitesData || sitesData.length === 0) sitesData = await apiCall('sites');
const sel = document.getElementById('ipam-form-site');
sel.innerHTML = '<option value="">— Ei sijaintia —</option>' +
sitesData.map(s => `<option value="${s.id}">${esc(s.nimi)}${s.kaupunki ? ' (' + esc(s.kaupunki) + ')' : ''}</option>`).join('');
} catch (e) { console.error(e); }
}
async function editIpam(id) {
const e = ipamData.find(x => x.id === id);
if (!e) return;
document.getElementById('ipam-form-id').value = e.id;
document.getElementById('ipam-form-tyyppi').value = e.tyyppi || 'ip';
document.getElementById('ipam-form-verkko').value = e.verkko || '';
document.getElementById('ipam-form-vlan').value = e.vlan_id || '';
document.getElementById('ipam-form-nimi').value = e.nimi || '';
document.getElementById('ipam-form-tila').value = e.tila || 'vapaa';
document.getElementById('ipam-form-asiakas').value = e.asiakas || '';
document.getElementById('ipam-form-lisatiedot').value = e.lisatiedot || '';
await loadIpamSitesDropdown();
document.getElementById('ipam-form-site').value = e.site_id || '';
document.getElementById('ipam-modal-title').textContent = 'Muokkaa IPAM-merkintää';
document.getElementById('ipam-modal').style.display = 'flex';
}
async function deleteIpam(id, name) {
if (!confirm(`Poistetaanko IPAM-merkintä "${name}"?`)) return;
try {
await apiCall('ipam_delete', 'POST', { id });
loadIpam();
} catch (e) { alert(e.message); }
}
document.getElementById('btn-add-ipam')?.addEventListener('click', async () => {
document.getElementById('ipam-form-id').value = '';
document.getElementById('ipam-form').reset();
await loadIpamSitesDropdown();
document.getElementById('ipam-modal-title').textContent = 'Lisää IPAM-merkintä';
document.getElementById('ipam-modal').style.display = 'flex';
});
document.getElementById('ipam-modal-close')?.addEventListener('click', () => {
document.getElementById('ipam-modal').style.display = 'none';
});
document.getElementById('ipam-form-cancel')?.addEventListener('click', () => {
document.getElementById('ipam-modal').style.display = 'none';
});
document.getElementById('ipam-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('ipam-form-id').value;
const data = {
tyyppi: document.getElementById('ipam-form-tyyppi').value,
verkko: document.getElementById('ipam-form-verkko').value.trim(),
vlan_id: document.getElementById('ipam-form-vlan').value || null,
nimi: document.getElementById('ipam-form-nimi').value.trim(),
site_id: document.getElementById('ipam-form-site').value || null,
tila: document.getElementById('ipam-form-tila').value,
asiakas: document.getElementById('ipam-form-asiakas').value.trim(),
lisatiedot: document.getElementById('ipam-form-lisatiedot').value.trim(),
};
if (id) data.id = id;
try {
await apiCall('ipam_save', 'POST', data);
document.getElementById('ipam-modal').style.display = 'none';
loadIpam();
} catch (e) { alert(e.message); }
});
document.getElementById('ipam-search-input')?.addEventListener('input', () => renderIpam());
// ==================== MODUULIT ====================
const ALL_MODULES = ['customers', 'support', 'leads', 'devices', 'archive', 'changelog', 'settings'];
const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'archive', 'changelog', 'settings'];
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
function applyModules(modules) {
// Yhteensopivuus: vanha 'devices' → 'tekniikka'
if (modules && modules.includes('devices') && !modules.includes('tekniikka')) {
modules = modules.map(m => m === 'devices' ? 'tekniikka' : m);
}
// Jos tyhjä array → kaikki moduulit päällä (fallback)
const enabled = (modules && modules.length > 0) ? modules : ALL_MODULES;
const isAdminUser = currentUser?.role === 'admin' || currentUser?.role === 'superadmin';